mirror of
https://github.com/SrIzan10/stack.git
synced 2026-06-27 19:12:19 +00:00
feat: initial v1 commit
This commit is contained in:
40
src/app/auth/signIn/page.tsx
Normal file
40
src/app/auth/signIn/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { useFormState } from "react-dom";
|
||||
import { login } from "@/lib/auth/actions";
|
||||
import SubmitButton from "@/components/app/SubmitButton/SubmitButton";
|
||||
|
||||
export default function Page() {
|
||||
const [, formAction] = useFormState(login, null);
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-4 lg:p-8">
|
||||
<div className="w-full max-w-md m-auto space-y-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold pb-1">Log In</h1>
|
||||
</div>
|
||||
<form action={formAction}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Username</Label>
|
||||
<Input name="username" id="username" required type="text" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input name="password" id="password" required type="password" />
|
||||
</div>
|
||||
<SubmitButton buttonText="Log In" className="w-full" />
|
||||
<div className="text-center text-sm">
|
||||
No account?
|
||||
<Link className="underline pl-1" href="/auth/signUp">
|
||||
Create one!
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/app/auth/signUp/page.tsx
Normal file
46
src/app/auth/signUp/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import Link from "next/link"
|
||||
import { signup } from "@/lib/auth/actions";
|
||||
import SubmitButton from "@/components/app/SubmitButton/SubmitButton";
|
||||
import { useFormState } from "react-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Page() {
|
||||
const [signupData, signupAction] = useFormState(signup, null)
|
||||
useEffect(() => {
|
||||
if (signupData?.error) {
|
||||
toast.error(signupData.error)
|
||||
}
|
||||
}, [signupData])
|
||||
return (
|
||||
<div className="flex items-center p-6 lg:p-8">
|
||||
<div className="w-full max-w-md m-auto space-y-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold pb-1">Sign Up</h1>
|
||||
</div>
|
||||
<form action={signupAction}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input name="username" required type="text" id="username" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input name="password" required type="password" id="password" />
|
||||
</div>
|
||||
<SubmitButton buttonText="Create account" className="w-full" />
|
||||
<div className="text-center text-sm">
|
||||
Already have an account?
|
||||
<Link className="underline pl-1" href="/auth/signIn">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
80
src/app/globals.css
Normal file
80
src/app/globals.css
Normal file
@@ -0,0 +1,80 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 220 23.077% 94.902%; /* base */
|
||||
--foreground: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--muted: 222.857 15.909% 82.745%; /* surface0 */
|
||||
--muted-foreground: 233.333 12.796% 41.373%; /* subtext1 */
|
||||
|
||||
--popover: 220 23.077% 94.902%; /* base */
|
||||
--popover-foreground: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--card: 220 23.077% 94.902%; /* base */
|
||||
--card-foreground: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--border: 225 13.559% 76.863%; /* surface1 */
|
||||
--input: 225 13.559% 76.863%; /* surface1 */
|
||||
|
||||
--primary: 219.907 91.489% 53.922%; /* blue */
|
||||
--primary-foreground: 220 23.077% 94.902%; /* base */
|
||||
|
||||
--secondary: 222.857 15.909% 82.745%; /* surface0 */
|
||||
--secondary-foreground: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--accent: 222.857 15.909% 82.745%; /* surface0 */
|
||||
--accent-foreground: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--destructive: 347.077 86.667% 44.118%; /* red */
|
||||
--destructive-foreground: 220 21.951% 91.961%; /* mantle */
|
||||
|
||||
--ring: 233.793 16.022% 35.490%; /* text */
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 21.053% 14.902%; /* base */
|
||||
--foreground: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--muted: 236.842 16.239% 22.941%; /* surface0 */
|
||||
--muted-foreground: 226.667 35.294% 80.000%; /* subtext1 */
|
||||
|
||||
--popover: 240 21.053% 14.902%; /* base */
|
||||
--popover-foreground: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--card: 240 21.053% 14.902%; /* base */
|
||||
--card-foreground: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--border: 234.286 13.208% 31.176%; /* surface1 */
|
||||
--input: 234.286 13.208% 31.176%; /* surface1 */
|
||||
|
||||
--primary: 217.168 91.870% 75.882%; /* blue */
|
||||
--primary-foreground: 240 21.053% 14.902%; /* base */
|
||||
|
||||
--secondary: 236.842 16.239% 22.941%; /* surface0 */
|
||||
--secondary-foreground: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--accent: 236.842 16.239% 22.941%; /* surface0 */
|
||||
--accent-foreground: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--destructive: 343.269 81.250% 74.902%; /* red */
|
||||
--destructive-foreground: 240 21.311% 11.961%; /* mantle */
|
||||
|
||||
--ring: 226.154 63.934% 88.039%; /* text */
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
41
src/app/layout.tsx
Normal file
41
src/app/layout.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navbar from "@/components/app/NavBar/NavBar";
|
||||
import { SessionProvider } from "@/lib/providers/SessionProvider";
|
||||
import { validateRequest } from "@/lib/auth";
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { ThemeProvider } from "@/lib/providers/ThemeProvider";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "stack",
|
||||
description: "The tech stack for your next(.js) project.",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const sessionData = await validateRequest()
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<SessionProvider value={sessionData}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Navbar />
|
||||
{children}
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
112
src/app/page.tsx
Normal file
112
src/app/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
// https://v0.dev/r/DxCSk58T8pM
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex-1">
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 xl:py-48">
|
||||
<div className="container px-4 md:px-6">
|
||||
<div className="flex flex-col items-center space-y-4 text-center">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-6xl/none">
|
||||
The modern tech stack for your next(.js) project
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[700px] text-gray-500 md:text-xl dark:text-gray-400">
|
||||
stack is a comprehensive tech stack that includes everything you need to build and deploy your next
|
||||
web application.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-4">
|
||||
<Link href="https://github.com/SrIzan10/stack">
|
||||
<Button>Start right NOW!</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 bg-gray-100 dark:bg-gray-900" id="features">
|
||||
<div className="container px-4 md:px-6">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-block rounded-lg bg-gray-100 px-3 py-1 text-sm dark:bg-gray-800">
|
||||
Key Features
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tighter sm:text-5xl">
|
||||
Everything you need to build and deploy
|
||||
</h2>
|
||||
<p className="max-w-[900px] text-gray-500 md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed dark:text-gray-400">
|
||||
Stack is a comprehensive tech stack that includes everything you need to build and deploy your next
|
||||
web application.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto max-w-5xl items-center gap-6 py-12 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="flex flex-col justify-center items-center text-center space-y-4">
|
||||
<ul className="grid gap-6">
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Next.js</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Build server-rendered React applications with Next.js.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Prisma</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Access your database with Prisma, the best way to work with databases in Ethan's opinion
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Tailwind CSS</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Style your application with Tailwind CSS, the utility-first CSS framework.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">shadcn/ui</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Develop your application with Vite, the next-generation frontend tooling.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Vercel</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Deploy your application to the cloud with Vercel, the serverless platform.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Cloudflare Pages</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Take advantage of the speed of Cloudflare to host your serverless websites.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="grid gap-1">
|
||||
<h3 className="text-xl font-bold">Lucia</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Manage authentication with Lucia auth, the best selfhosted authentication library.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
src/app/protected/page.tsx
Normal file
14
src/app/protected/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { validateRequest } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
const { user } = await validateRequest()
|
||||
if (!user) return redirect('/auth/signIn')
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-center">Welcome {user?.username}!</h1>
|
||||
<p>You are actually on a protected route!</p>
|
||||
<p>Your ID is: {user.id}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
src/components/app/MobileNavbarLinks/MobileNavbarLinks.tsx
Normal file
35
src/components/app/MobileNavbarLinks/MobileNavbarLinks.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { links } from "../NavBar/NavBar";
|
||||
|
||||
export default function MobileNavbarLinks() {
|
||||
return (
|
||||
<div className="flex md:hidden">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>stack</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{links.map((link) => (
|
||||
<Link key={link.href} href={link.href}>
|
||||
<DropdownMenuItem>{link.name}</DropdownMenuItem>
|
||||
</Link>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
82
src/components/app/NavBar/NavBar.tsx
Normal file
82
src/components/app/NavBar/NavBar.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client"
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
/**
|
||||
* v0 by Vercel.
|
||||
* @see https://v0.dev/t/igzEEdGqAvH
|
||||
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
|
||||
*/
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
||||
import { logout } from "@/lib/auth/actions"
|
||||
import { useSession } from "@/lib/providers/SessionProvider"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { useFormState } from "react-dom"
|
||||
import MobileNavbarLinks from "../MobileNavbarLinks/MobileNavbarLinks"
|
||||
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher"
|
||||
|
||||
export const links = [
|
||||
{ href: '/', name: 'Home' },
|
||||
{ href: 'https://github.com/SrIzan10/stack', name: 'Github' },
|
||||
{ href: '/protected', name: 'Protected route' }
|
||||
]
|
||||
|
||||
function NavbarLinks() {
|
||||
return (
|
||||
<>
|
||||
{links.map((link) => (
|
||||
<Link key={link.href} href={link.href}>
|
||||
<Button variant={'link'}>{link.name}</Button>
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
const { user } = useSession();
|
||||
const [, logoutAction] = useFormState(logout, null)
|
||||
return (
|
||||
<>
|
||||
<nav className="flex items-center h-16 px-4 border-b gap-3 shrink-0">
|
||||
<Link href="/" className="hidden md:flex">
|
||||
<Button>stack</Button>
|
||||
</Link>
|
||||
<MobileNavbarLinks />
|
||||
<div className="hidden md:flex">
|
||||
<NavbarLinks />
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<ThemeSwitcher />
|
||||
{user ? (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="cursor-pointer">
|
||||
<Avatar>
|
||||
<AvatarImage src={"https://srizan.dev/pfp.webp"} alt="@srizan" />
|
||||
<AvatarFallback>SI</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => {
|
||||
logoutAction()
|
||||
}}>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/auth/signIn">
|
||||
<Button variant="outline">Sign in</Button>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
18
src/components/app/SubmitButton/SubmitButton.tsx
Normal file
18
src/components/app/SubmitButton/SubmitButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { Button, ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
import { VariantProps } from "class-variance-authority"
|
||||
import { useFormStatus } from "react-dom"
|
||||
|
||||
export default function SubmitButton(props: Props) {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" loading={pending} {...props}>
|
||||
{props.buttonText}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
buttonText: string;
|
||||
}
|
||||
40
src/components/app/ThemeSwitcher/ThemeSwitcher.tsx
Normal file
40
src/components/app/ThemeSwitcher/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
63
src/components/ui/button.tsx
Normal file
63
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LoaderCircle } from "lucide-react"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
defaultOutlineHover:
|
||||
"border border-input bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10"
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, loading, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
disabled={loading}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{loading && <LoaderCircle className={cn('h-4 w-4 animate-spin', props.children && 'mr-2')} />}
|
||||
{props.children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
200
src/components/ui/dropdown-menu.tsx
Normal file
200
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
31
src/components/ui/sonner.tsx
Normal file
31
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
125
src/lib/auth/actions.ts
Normal file
125
src/lib/auth/actions.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
"use server"
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { lucia, validateRequest } from ".";
|
||||
import { redirect } from "next/navigation";
|
||||
import prisma from "../db";
|
||||
import { Argon2id } from "oslo/password";
|
||||
import { generateId } from "lucia";
|
||||
|
||||
export async function logout() {
|
||||
const { session } = await validateRequest();
|
||||
await lucia.invalidateSession(session!.id);
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return redirect("/auth/signIn");
|
||||
}
|
||||
|
||||
export async function login(prev: any, data: FormData) {
|
||||
const username = data.get("username");
|
||||
if (
|
||||
typeof username !== "string" ||
|
||||
username.length < 3 ||
|
||||
username.length > 31 ||
|
||||
!/^[a-z0-9_-]+$/.test(username)
|
||||
) {
|
||||
return {
|
||||
error: "Invalid username",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
const password = data.get("password");
|
||||
if (typeof password !== "string" || password.length < 6 || password.length > 255) {
|
||||
return {
|
||||
error: "Invalid password",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: username
|
||||
}
|
||||
})
|
||||
if (!existingUser) {
|
||||
// NOTE:
|
||||
// Returning immediately allows malicious actors to figure out valid usernames from response times,
|
||||
// allowing them to only focus on guessing passwords in brute-force attacks.
|
||||
// As a preventive measure, you may want to hash passwords even for invalid usernames.
|
||||
// However, valid usernames can be already be revealed with the signup page among other methods.
|
||||
// It will also be much more resource intensive.
|
||||
// Since protecting against this is non-trivial,
|
||||
// it is crucial your implementation is protected against brute-force attacks with login throttling etc.
|
||||
// If usernames are public, you may outright tell the user that the username is invalid.
|
||||
return {
|
||||
error: "Incorrect username or password",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const validPassword = await new Argon2id().verify(existingUser.hashed_password, password);
|
||||
if (!validPassword) {
|
||||
return {
|
||||
error: "Incorrect username or password",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const session = await lucia.createSession(existingUser.id, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
export async function signup(prev: any, formData: FormData): Promise<ActionResult> {
|
||||
"use server";
|
||||
const username = formData.get("username");
|
||||
console.log(username)
|
||||
// username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _
|
||||
// keep in mind some database (e.g. mysql) are case insensitive
|
||||
if (
|
||||
typeof username !== "string" ||
|
||||
username.length < 3 ||
|
||||
username.length > 31 ||
|
||||
!/^[a-z0-9_-]+$/.test(username)
|
||||
) {
|
||||
return {
|
||||
error: "Invalid username",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
if (await prisma.user.findUnique({ where: { username: username } })) {
|
||||
return {
|
||||
error: "Username is already taken",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
const password = formData.get("password");
|
||||
if (typeof password !== "string" || password.length < 6 || password.length > 255) {
|
||||
return {
|
||||
error: "Invalid password",
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const hashedPassword = await new Argon2id().hash(password);
|
||||
const userId = generateId(15);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
username: username,
|
||||
hashed_password: hashedPassword
|
||||
}
|
||||
});
|
||||
|
||||
const session = await lucia.createSession(userId, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
interface ActionResult {
|
||||
error: string | null;
|
||||
success: boolean | null;
|
||||
}
|
||||
71
src/lib/auth/index.ts
Normal file
71
src/lib/auth/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import { Lucia, Session, User } from "lucia";
|
||||
import prisma from "../db";
|
||||
import { cache } from "react";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const adapter = new PrismaAdapter(prisma.session, prisma.user);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
// this sets cookies with super long expiration
|
||||
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
|
||||
expires: false,
|
||||
attributes: {
|
||||
// set to `true` when using HTTPS
|
||||
secure: process.env.NODE_ENV === "production"
|
||||
}
|
||||
},
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
username: attributes.username
|
||||
};
|
||||
}
|
||||
});
|
||||
export const validateRequest = cache(async () => {
|
||||
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null
|
||||
|
||||
if (!sessionId)
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
}
|
||||
|
||||
const { user, session } = await lucia.validateSession(sessionId)
|
||||
try {
|
||||
if (session && session.fresh) {
|
||||
const sessionCookie = lucia.createSessionCookie(session.id)
|
||||
cookies().set(
|
||||
sessionCookie.name,
|
||||
sessionCookie.value,
|
||||
sessionCookie.attributes
|
||||
)
|
||||
}
|
||||
if (!session) {
|
||||
const sessionCookie = lucia.createBlankSessionCookie()
|
||||
cookies().set(
|
||||
sessionCookie.name,
|
||||
sessionCookie.value,
|
||||
sessionCookie.attributes
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Next.js throws error attempting to set cookies when rendering page
|
||||
}
|
||||
return {
|
||||
user,
|
||||
session,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
declare module "lucia" {
|
||||
interface Register {
|
||||
Lucia: typeof lucia;
|
||||
DatabaseUserAttributes: DatabaseUserAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseUserAttributes {
|
||||
username: string;
|
||||
}
|
||||
15
src/lib/db/index.ts
Normal file
15
src/lib/db/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient()
|
||||
}
|
||||
|
||||
declare global {
|
||||
var prismaGlobal: undefined | ReturnType<typeof prismaClientSingleton>
|
||||
}
|
||||
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
|
||||
export default prisma
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||
36
src/lib/providers/SessionProvider.tsx
Normal file
36
src/lib/providers/SessionProvider.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// source: https://github.com/ugurkellecioglu/next-14-lucia-auth-postgresql-drizzle-typescript-example/blob/lucia-client-side/providers/Session.provider.tsx
|
||||
|
||||
"use client"
|
||||
import { Session, User } from "lucia"
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
interface SessionProviderProps {
|
||||
user: User | null
|
||||
session: Session | null
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionProviderProps>(
|
||||
{} as SessionProviderProps
|
||||
)
|
||||
|
||||
export const SessionProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
value: SessionProviderProps
|
||||
}) => {
|
||||
return (
|
||||
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useSession = () => {
|
||||
const sessionContext = useContext(SessionContext)
|
||||
|
||||
if (!sessionContext) {
|
||||
throw new Error("useSession must be used within a SessionProvider")
|
||||
}
|
||||
|
||||
return sessionContext
|
||||
}
|
||||
9
src/lib/providers/ThemeProvider.tsx
Normal file
9
src/lib/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user