diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e565acd..f61b4d6 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.1.0",
"dependencies": {
+ "lucide-react": "^1.18.0",
"next": "16.2.9",
"react": "19.2.4",
"react-dom": "19.2.4"
@@ -4961,6 +4962,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz",
+ "integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index fe877a3..03a9b9e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"format:check": "prettier --check ."
},
"dependencies": {
+ "lucide-react": "^1.18.0",
"next": "16.2.9",
"react": "19.2.4",
"react-dom": "19.2.4"
diff --git a/frontend/src/app/(portal)/admin/builder/page.tsx b/frontend/src/app/(portal)/admin/builder/page.tsx
new file mode 100644
index 0000000..a537585
--- /dev/null
+++ b/frontend/src/app/(portal)/admin/builder/page.tsx
@@ -0,0 +1,7 @@
+export default function BuilderPage() {
+ return (
+
+
App Builder
+
+ )
+}
diff --git a/frontend/src/app/(portal)/admin/cycles/page.tsx b/frontend/src/app/(portal)/admin/cycles/page.tsx
new file mode 100644
index 0000000..702bf28
--- /dev/null
+++ b/frontend/src/app/(portal)/admin/cycles/page.tsx
@@ -0,0 +1,7 @@
+export default function CyclesPage() {
+ return (
+
+
Cycles
+
+ )
+}
diff --git a/frontend/src/app/(portal)/admin/roles/page.tsx b/frontend/src/app/(portal)/admin/roles/page.tsx
new file mode 100644
index 0000000..1c4a3e1
--- /dev/null
+++ b/frontend/src/app/(portal)/admin/roles/page.tsx
@@ -0,0 +1,7 @@
+export default function RolesPage() {
+ return (
+
+
Roles
+
+ )
+}
diff --git a/frontend/src/app/(portal)/applicant/applications/page.tsx b/frontend/src/app/(portal)/applicant/applications/page.tsx
new file mode 100644
index 0000000..143adc7
--- /dev/null
+++ b/frontend/src/app/(portal)/applicant/applications/page.tsx
@@ -0,0 +1,7 @@
+export default function ApplicationsPage() {
+ return (
+
+
My Applications
+
+ )
+}
diff --git a/frontend/src/app/(portal)/layout.tsx b/frontend/src/app/(portal)/layout.tsx
new file mode 100644
index 0000000..a9e2148
--- /dev/null
+++ b/frontend/src/app/(portal)/layout.tsx
@@ -0,0 +1,25 @@
+import Sidebar from '@/components/nav/Sidebar'
+import { mockUser } from '@/lib/mock-user'
+import { getRoles } from '@/types/roles'
+
+export default function PortalLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ // TODO: replace mockUser with session user from auth
+ const roles = getRoles(mockUser)
+
+ return (
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/frontend/src/app/(portal)/reviewer/applicants/page.tsx b/frontend/src/app/(portal)/reviewer/applicants/page.tsx
new file mode 100644
index 0000000..b0d91ff
--- /dev/null
+++ b/frontend/src/app/(portal)/reviewer/applicants/page.tsx
@@ -0,0 +1,7 @@
+export default function ApplicantsPage() {
+ return (
+
+
Applicants
+
+ )
+}
diff --git a/frontend/src/app/(portal)/reviewer/dashboard/page.tsx b/frontend/src/app/(portal)/reviewer/dashboard/page.tsx
new file mode 100644
index 0000000..7a594d1
--- /dev/null
+++ b/frontend/src/app/(portal)/reviewer/dashboard/page.tsx
@@ -0,0 +1,7 @@
+export default function DashboardPage() {
+ return (
+
+
Dashboard
+
+ )
+}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 37d72f8..5628d86 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -8,6 +8,8 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
+ --color-brand-blue: #1477f8;
+ --color-brand-white: #ffffff;
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index bcdb60c..03ed81d 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
})
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
+ title: 'Generate Portal',
+ description: 'Application portal for Generate NU',
}
export default function RootLayout({
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index f958f5e..d65b2c3 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -1,65 +1,5 @@
-import Image from 'next/image'
+import { redirect } from 'next/navigation'
-export default function Home() {
- return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{' '}
-
- Templates
- {' '}
- or the{' '}
-
- Learning
- {' '}
- center.
-
-
-
-
-
- )
+export default function RootPage() {
+ redirect('/reviewer/dashboard')
}
diff --git a/frontend/src/components/nav/NavItem.tsx b/frontend/src/components/nav/NavItem.tsx
new file mode 100644
index 0000000..0b6652c
--- /dev/null
+++ b/frontend/src/components/nav/NavItem.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import type { LucideIcon } from 'lucide-react'
+
+interface NavItemProps {
+ href: string
+ label: string
+ icon: LucideIcon
+}
+
+export default function NavItem({ href, label, icon: Icon }: NavItemProps) {
+ const pathname = usePathname()
+ const isActive = pathname === href || pathname.startsWith(href + '/')
+
+ return (
+
+
+ {label}
+
+ )
+}
diff --git a/frontend/src/components/nav/Sidebar.tsx b/frontend/src/components/nav/Sidebar.tsx
new file mode 100644
index 0000000..d5b981f
--- /dev/null
+++ b/frontend/src/components/nav/Sidebar.tsx
@@ -0,0 +1,119 @@
+'use client'
+
+import {
+ LayoutDashboard,
+ Users,
+ FileText,
+ RefreshCw,
+ Settings,
+ Layers,
+} from 'lucide-react'
+import NavItem from './NavItem'
+import type { Role } from '@/types/roles'
+
+interface SidebarProps {
+ roles: Role[]
+ firstName?: string
+ lastName?: string
+}
+
+type NavSection = {
+ label: string
+ items: { href: string; label: string; icon: typeof FileText }[]
+}
+
+const sectionsByRole: Record = {
+ applicant: {
+ label: 'Applications',
+ items: [
+ {
+ href: '/applicant/applications',
+ label: 'My Applications',
+ icon: FileText,
+ },
+ ],
+ },
+ reviewer: {
+ label: 'Review',
+ items: [
+ {
+ href: '/reviewer/dashboard',
+ label: 'Dashboard',
+ icon: LayoutDashboard,
+ },
+ { href: '/reviewer/applicants', label: 'Applicants', icon: Users },
+ ],
+ },
+ admin: {
+ label: 'Admin',
+ items: [
+ { href: '/admin/cycles', label: 'Cycles', icon: RefreshCw },
+ { href: '/admin/builder', label: 'App Builder', icon: Layers },
+ { href: '/admin/roles', label: 'Roles', icon: Settings },
+ ],
+ },
+}
+
+// Display order: reviewer sections before applicant, admin last
+const roleOrder: Role[] = ['reviewer', 'applicant', 'admin']
+
+function SidebarUser({
+ firstName,
+ lastName,
+}: {
+ firstName: string
+ lastName: string
+}) {
+ return (
+
+
+
+ {firstName[0]}
+ {lastName[0]}
+
+
+ {firstName} {lastName}
+
+
+
+ )
+}
+
+export default function Sidebar({ roles, firstName, lastName }: SidebarProps) {
+ const sections = roleOrder
+ .filter((role) => roles.includes(role))
+ .map((role) => sectionsByRole[role])
+
+ return (
+
+ )
+}
diff --git a/frontend/src/lib/mock-user.ts b/frontend/src/lib/mock-user.ts
new file mode 100644
index 0000000..07d2353
--- /dev/null
+++ b/frontend/src/lib/mock-user.ts
@@ -0,0 +1,9 @@
+import type { User } from '@/types/user'
+
+export const mockUser: User = {
+ nuid: '002139999',
+ first_name: 'First',
+ last_name: 'Last',
+ is_reviewer: true,
+ is_admin: false,
+}
diff --git a/frontend/src/types/roles.ts b/frontend/src/types/roles.ts
new file mode 100644
index 0000000..e2fdf85
--- /dev/null
+++ b/frontend/src/types/roles.ts
@@ -0,0 +1,10 @@
+import type { User } from './user'
+
+export type Role = 'applicant' | 'reviewer' | 'admin'
+
+export function getRoles(user: User): Role[] {
+ const roles: Role[] = ['applicant']
+ if (user.is_reviewer) roles.push('reviewer')
+ if (user.is_admin) roles.push('admin')
+ return roles
+}
diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts
new file mode 100644
index 0000000..ca56993
--- /dev/null
+++ b/frontend/src/types/user.ts
@@ -0,0 +1,7 @@
+export interface User {
+ nuid: string
+ first_name: string
+ last_name: string
+ is_reviewer: boolean
+ is_admin: boolean
+}