Skip to content

Commit 92eaac4

Browse files
committed
feat: add landing page
1 parent 9a7e0dd commit 92eaac4

File tree

7 files changed

+301
-16
lines changed

7 files changed

+301
-16
lines changed

app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export default function RootLayout({
2727
return (
2828
<html lang="en">
2929
<body
30-
className={`${geistSans.variable} ${geistMono.variable} antialiased container mx-auto px-4`}
30+
// container mx-auto px-4
31+
className={`${geistSans.variable} ${geistMono.variable} antialiased mx-auto`}
3132
>
3233
{children}
3334
<Toaster />

app/page.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,62 @@ import {
99
BookCopy,
1010
} from 'lucide-react'
1111
import { Button } from '@/components/ui/button'
12+
import { IsLoggedIn } from '@/lib/firebase/firebase'
13+
import Landing from '@/components/landing'
1214

13-
export default function LibraryDashboard() {
14-
const menuItems = [
15-
{ title: 'Libraries', icon: Library, href: '/libraries' },
16-
{ title: 'Books', icon: Book, href: '/books' },
17-
{ title: 'Users', icon: Users, href: '/users' },
18-
{ title: 'Staffs', icon: UserCog, href: '/staffs' },
19-
{ title: 'Memberships', icon: CreditCard, href: '/memberships' },
20-
{ title: 'Subscriptions', icon: ScrollText, href: '/subscriptions' },
21-
{ title: 'Borrows', icon: BookCopy, href: '/borrows' },
22-
]
15+
const menuItems = [
16+
{ title: 'Libraries', icon: Library, href: '/libraries', level: 1 },
17+
{ title: 'Books', icon: Book, href: '/books', level: 1 },
18+
{ title: 'Users', icon: Users, href: '/users', level: 3 },
19+
{ title: 'Staffs', icon: UserCog, href: '/staffs', level: 3 },
20+
{ title: 'Memberships', icon: CreditCard, href: '/memberships', level: 3 },
21+
{
22+
title: 'Subscriptions',
23+
icon: ScrollText,
24+
href: '/subscriptions',
25+
level: 3,
26+
},
27+
{ title: 'Borrows', icon: BookCopy, href: '/borrows', level: 3 },
28+
{
29+
title: 'My Memberships',
30+
icon: CreditCard,
31+
href: '/memberships/me',
32+
level: 2,
33+
},
34+
{
35+
title: 'My Subscriptions',
36+
icon: ScrollText,
37+
href: '/subscriptions/me',
38+
level: 2,
39+
},
40+
{ title: 'My Borrows', icon: BookCopy, href: '/borrows/me', level: 2 },
41+
]
42+
43+
export default async function LibraryDashboard() {
44+
const claim = await IsLoggedIn()
45+
46+
// TODO: remove after the custom claim is set
47+
if (!claim || !claim.librarease) {
48+
return <Landing />
49+
}
50+
51+
const userLvl =
52+
claim.librarease.role === 'SUPERADMIN'
53+
? 5
54+
: claim.librarease.role === 'ADMIN'
55+
? 4
56+
: claim.librarease.admin_libs.length > 0 ||
57+
claim.librarease.staff_libs.length > 0
58+
? 3
59+
: 2
2360

2461
return (
2562
<main className="min-h-screen bg-white p-8">
2663
<div className="max-w-2xl mx-auto">
2764
<h1 className="text-2xl font-bold mb-8">Library Management</h1>
2865
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
2966
{menuItems.map((item) => {
67+
if (item.level > userLvl) return null
3068
const Icon = item.icon
3169
return (
3270
<Link key={item.href} href={item.href}>

components/landing.tsx

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import Link from 'next/link'
2+
import {
3+
ArrowRight,
4+
BookUser,
5+
Users,
6+
BookCheck,
7+
TicketPlus,
8+
Library,
9+
BookCopy,
10+
CreditCard,
11+
CalendarClock,
12+
} from 'lucide-react'
13+
import { Button } from '@/components/ui/button'
14+
import {
15+
Card,
16+
CardContent,
17+
CardDescription,
18+
CardHeader,
19+
CardTitle,
20+
} from '@/components/ui/card'
21+
22+
export default function LandingPage() {
23+
// In a real app, these would come from an API
24+
const stats = [
25+
{ label: 'Libraries', value: '25+', icon: Library },
26+
{ label: 'Active Users', value: '10,000+', icon: Users },
27+
{ label: 'Books Available', value: '100,000+', icon: BookCheck },
28+
{ label: 'Daily Borrows', value: '500+', icon: BookCopy },
29+
]
30+
31+
const features = [
32+
{
33+
title: 'Effortless Browsing',
34+
description:
35+
'Find books across libraries in seconds—no more endless searches',
36+
icon: BookCheck,
37+
},
38+
{
39+
title: 'Reserve & Collect',
40+
description: "Reserve books online, pick them up when it's convenient",
41+
icon: TicketPlus,
42+
},
43+
{
44+
title: 'Tailored Memberships',
45+
description: 'Borrow the way that fits you, with options for everyone',
46+
icon: CreditCard,
47+
},
48+
{
49+
title: 'Stay on Top of It',
50+
description: 'Easy tracking for due dates, renewals, and past loans',
51+
icon: CalendarClock,
52+
},
53+
]
54+
55+
return (
56+
<div className="flex flex-col min-h-screen">
57+
<header className="px-4 lg:px-6 h-14 flex items-center border-b justify-between">
58+
<Link className="flex items-center justify-center" href="/">
59+
<BookUser className="h-6 w-6" />
60+
<span className="ml-2 text-lg font-bold">Librarease</span>
61+
</Link>
62+
<nav className="flex gap-4 sm:gap-6">
63+
<Button variant="ghost" asChild>
64+
<Link href="/login">Sign In</Link>
65+
</Button>
66+
<Button asChild>
67+
<Link href="/signup">Get Started</Link>
68+
</Button>
69+
</nav>
70+
</header>
71+
<main className="flex-1">
72+
<section className="w-full py-12 md:py-24 lg:py-32 xl:py-48 bg-slate-50">
73+
<div className="container px-4 md:px-6 mx-auto">
74+
<div className="flex flex-col items-center space-y-4 text-center">
75+
<div className="space-y-2">
76+
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl/none">
77+
Your Borrowing, Simplified
78+
</h1>
79+
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl">
80+
Discover a hassle-free way to browse and reserve books online.
81+
Borrow what you need, then pick it up from your library—on
82+
your schedule.
83+
</p>
84+
</div>
85+
<div className="space-y-2 md:space-x-4">
86+
<Button size="lg" asChild>
87+
<Link href="/signup">
88+
Start Borrowing Today
89+
<ArrowRight className="ml-2 h-4 w-4" />
90+
</Link>
91+
</Button>
92+
<Button variant="outline" size="lg" asChild>
93+
<Link href="/about">Explore Features</Link>
94+
</Button>
95+
</div>
96+
</div>
97+
</div>
98+
</section>
99+
100+
<section className="w-full py-12 md:py-24 lg:py-32">
101+
<div className="container px-4 md:px-6 mx-auto">
102+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
103+
{stats.map((stat) => {
104+
const Icon = stat.icon
105+
return (
106+
<Card key={stat.label} className="relative overflow-hidden">
107+
<CardHeader>
108+
<Icon className="h-8 w-8 text-primary" />
109+
</CardHeader>
110+
<CardContent>
111+
<div className="text-3xl font-bold">{stat.value}</div>
112+
<p className="text-sm text-gray-500">{stat.label}</p>
113+
</CardContent>
114+
</Card>
115+
)
116+
})}
117+
</div>
118+
</div>
119+
</section>
120+
121+
<section className="w-full py-12 md:py-24 lg:py-32 bg-slate-50">
122+
<div className="container px-4 md:px-6 mx-auto">
123+
<div className="flex flex-col items-center justify-center space-y-4 text-center">
124+
<div className="space-y-2">
125+
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl">
126+
Why Librarease?
127+
</h2>
128+
<p className="max-w-[900px] text-gray-500 md:text-xl">
129+
A smarter, simpler way to connect with your library.
130+
</p>
131+
</div>
132+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4 mt-8">
133+
{features.map((feature) => {
134+
const Icon = feature.icon
135+
return (
136+
<Card
137+
key={feature.title}
138+
className="relative overflow-hidden"
139+
>
140+
<CardHeader>
141+
<Icon className="h-8 w-8 text-primary" />
142+
<CardTitle className="text-lg">
143+
{feature.title}
144+
</CardTitle>
145+
</CardHeader>
146+
<CardContent>
147+
<CardDescription>{feature.description}</CardDescription>
148+
</CardContent>
149+
</Card>
150+
)
151+
})}
152+
</div>
153+
</div>
154+
</div>
155+
</section>
156+
157+
<section className="w-full py-12 md:py-24 lg:py-32 border-t">
158+
<div className="container px-4 md:px-6 mx-auto">
159+
<div className="flex flex-col items-center justify-center space-y-4 text-center">
160+
<div className="space-y-2">
161+
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl">
162+
Ready to Start?
163+
</h2>
164+
<p className="max-w-[600px] text-gray-500 md:text-xl">
165+
Join our community today and get access to all features.
166+
</p>
167+
</div>
168+
<Button size="lg" asChild>
169+
<Link href="/signup">
170+
Create Free Account
171+
<ArrowRight className="ml-2 h-4 w-4" />
172+
</Link>
173+
</Button>
174+
</div>
175+
</div>
176+
</section>
177+
</main>
178+
<footer className="flex flex-col gap-2 sm:flex-row py-6 w-full shrink-0 items-center px-4 md:px-6 border-t">
179+
<p className="text-xs text-gray-500">
180+
© {new Date().getFullYear()} Librarease. All rights reserved.
181+
</p>
182+
<nav className="sm:ml-auto flex gap-4 sm:gap-6">
183+
<Link className="text-xs hover:underline underline-offset-4" href="#">
184+
Terms of Service
185+
</Link>
186+
<Link className="text-xs hover:underline underline-offset-4" href="#">
187+
Privacy
188+
</Link>
189+
</nav>
190+
</footer>
191+
</div>
192+
)
193+
}

components/login-form.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function LoginForm({
6464
id="email"
6565
type="email"
6666
name="email"
67-
// placeholder="m@example.com"
67+
placeholder="e.g. mgmg@example.com"
6868
defaultValue={state.email}
6969
required
7070
/>
@@ -75,6 +75,7 @@ export function LoginForm({
7575
id="password"
7676
name="password"
7777
type={showPassword ? 'text' : 'password'}
78+
placeholder="e.g. mypassword"
7879
// defaultValue={state.password}
7980
required
8081
/>

components/signup-form.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function SignUpForm({
5252
id="name"
5353
type="name"
5454
name="name"
55+
placeholder="e.g. Mg Mg"
5556
defaultValue={state.name}
5657
required
5758
/>
@@ -62,7 +63,7 @@ export function SignUpForm({
6263
id="email"
6364
type="email"
6465
name="email"
65-
// placeholder="m@example.com"
66+
placeholder="e.g. mgmg@example.com"
6667
defaultValue={state.email}
6768
required
6869
/>
@@ -73,6 +74,7 @@ export function SignUpForm({
7374
id="password"
7475
name="password"
7576
type="password"
77+
placeholder="Set a password"
7678
// defaultValue={state.password}
7779
required
7880
/>

lib/actions/login.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ export async function login(
4040

4141
const user = userCredentials.user
4242
const sessionName = process.env.SESSION_COOKIE_NAME as string
43-
const token = await user.getIdToken()
43+
const result = await user.getIdTokenResult()
44+
const token = result.token
45+
const maxAge =
46+
new Date(result.expirationTime).getTime() - new Date().getTime()
4447
const cookieStore = await cookies()
4548
cookieStore.set(sessionName, token, {
46-
maxAge: 60 * 60 * 24 * 7,
49+
maxAge,
4750
httpOnly: true,
4851
})
4952

lib/firebase/firebase.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { cookies } from 'next/headers'
22
import { adminApp } from './admin'
33
import { redirect, RedirectType } from 'next/navigation'
4+
import type { DecodedIdToken } from 'firebase-admin/auth'
45

5-
export async function Verify({ from }: { from: string }) {
6+
/**
7+
* Verify checks if the cookie
8+
* is valid and returns the uid & internal client id headers
9+
* redirect to login page if the cookie is invalid
10+
*/
11+
export async function Verify({ from }: { from: string }): Promise<Headers> {
612
const cookieStore = await cookies()
713
const sessionName = process.env.SESSION_COOKIE_NAME as string
814
const session = cookieStore.get(sessionName)
@@ -40,3 +46,44 @@ export async function Verify({ from }: { from: string }) {
4046

4147
return headers
4248
}
49+
50+
/**
51+
* IsLoggedIn checks if the user is logged in
52+
* and returns a token claims if the user is logged in
53+
*/
54+
export async function IsLoggedIn(): Promise<
55+
| (DecodedIdToken & {
56+
librarease: {
57+
id: string
58+
role: string
59+
admin_libs: string[]
60+
staff_libs: string[]
61+
}
62+
})
63+
| null
64+
> {
65+
const cookieStore = await cookies()
66+
const sessionName = process.env.SESSION_COOKIE_NAME as string
67+
const session = cookieStore.get(sessionName)
68+
69+
// return early if the session is not found
70+
if (!session) {
71+
return null
72+
}
73+
74+
try {
75+
const claims = await adminApp.auth().verifyIdToken(session?.value as string)
76+
return {
77+
librarease: {
78+
id: '',
79+
role: '',
80+
admin_libs: [],
81+
staff_libs: [],
82+
},
83+
...claims,
84+
}
85+
} catch (error) {
86+
console.log(error)
87+
return null
88+
}
89+
}

0 commit comments

Comments
 (0)