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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{' '} - - Templates - {' '} - or the{' '} - - Learning - {' '} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ) +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 +}