Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/admin/builder/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function BuilderPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">App Builder</h1>
</div>
)
}
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/admin/cycles/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function CyclesPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">Cycles</h1>
</div>
)
}
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/admin/roles/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function RolesPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">Roles</h1>
</div>
)
}
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/applicant/applications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function ApplicationsPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">My Applications</h1>
</div>
)
}
25 changes: 25 additions & 0 deletions frontend/src/app/(portal)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex h-screen">
<Sidebar
roles={roles}
firstName={mockUser.first_name}
lastName={mockUser.last_name}
/>
<main className="flex flex-1 flex-col overflow-y-auto bg-gray-50">
{children}
</main>
</div>
)
}
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/reviewer/applicants/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function ApplicantsPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">Applicants</h1>
</div>
)
}
7 changes: 7 additions & 0 deletions frontend/src/app/(portal)/reviewer/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function DashboardPage() {
return (
<div className="p-8">
<h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
</div>
)
}
2 changes: 2 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
66 changes: 3 additions & 63 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,5 @@
import Image from 'next/image'
import { redirect } from 'next/navigation'

export default function Home() {
return (
<div className="flex flex-1 flex-col items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex w-full max-w-3xl flex-1 flex-col items-center justify-between bg-white px-16 py-32 sm:items-start dark:bg-black">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl leading-10 font-semibold tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{' '}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{' '}
or the{' '}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{' '}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="bg-foreground text-background flex h-12 w-full items-center justify-center gap-2 rounded-full px-5 transition-colors hover:bg-[#383838] md:w-[158px] dark:hover:bg-[#ccc]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] md:w-[158px] dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
)
export default function RootPage() {
redirect('/reviewer/dashboard')
}
33 changes: 33 additions & 0 deletions frontend/src/components/nav/NavItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
href={href}
className={`flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
isActive
? 'text-brand-blue bg-blue-50'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
<Icon
size={16}
className={isActive ? 'text-brand-blue' : 'text-gray-400'}
/>
{label}
</Link>
)
}
119 changes: 119 additions & 0 deletions frontend/src/components/nav/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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<Role, NavSection> = {
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 (
<div className="border-t border-gray-100 px-4 py-3">
<div className="flex items-center gap-2.5">
<div className="bg-brand-blue flex h-7 w-7 items-center justify-center rounded-full text-xs font-semibold text-white">
{firstName[0]}
{lastName[0]}
</div>
<span className="text-sm font-medium text-gray-700">
{firstName} {lastName}
</span>
</div>
</div>
)
}

export default function Sidebar({ roles, firstName, lastName }: SidebarProps) {
const sections = roleOrder
.filter((role) => roles.includes(role))
.map((role) => sectionsByRole[role])

return (
<aside className="flex h-screen w-60 flex-col border-r border-gray-100 bg-white">
{/* Logo */}
<div className="flex h-11 items-center gap-2 border-b border-gray-100 px-4">
<div className="bg-brand-blue flex h-7 w-7 items-center justify-center rounded-md">
<span className="text-xs font-bold text-white">G</span>
</div>
<span className="text-sm font-semibold text-gray-900">Generate</span>
</div>

{/* Nav sections */}
<nav className="flex flex-1 flex-col gap-4 overflow-y-auto px-3 py-4">
{sections.map((section) => (
<div key={section.label}>
<p className="mb-1 px-3 text-xs font-medium tracking-wider text-gray-400 uppercase">
{section.label}
</p>
<div className="flex flex-col gap-0.5">
{section.items.map((item) => (
<NavItem key={item.href} {...item} />
))}
</div>
</div>
))}
</nav>

{/* User */}
{firstName && lastName && (
<SidebarUser firstName={firstName} lastName={lastName} />
)}
</aside>
)
}
9 changes: 9 additions & 0 deletions frontend/src/lib/mock-user.ts
Original file line number Diff line number Diff line change
@@ -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,
}
10 changes: 10 additions & 0 deletions frontend/src/types/roles.ts
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions frontend/src/types/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface User {
nuid: string
first_name: string
last_name: string
is_reviewer: boolean
is_admin: boolean
}
Loading