feat: design home page ui

This commit is contained in:
Tuluobo 2024-09-26 23:07:25 +08:00
parent ff0baffce8
commit 3e85d0828b
14 changed files with 291 additions and 66 deletions

View File

@ -2,6 +2,14 @@
const nextConfig = {
reactStrictMode: true,
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "shuzimumin.com",
},
],
},
};
export default nextConfig;

View File

@ -18,6 +18,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.4.2",
"@prisma/client": "^5.19.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",

34
pnpm-lock.yaml generated
View File

@ -14,6 +14,9 @@ importers:
'@prisma/client':
specifier: ^5.19.0
version: 5.19.0(prisma@5.19.0)
'@radix-ui/react-avatar':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-checkbox':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -418,6 +421,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-avatar@1.1.0':
resolution: {integrity: sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.1.1':
resolution: {integrity: sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==}
peerDependencies:
@ -2849,6 +2865,18 @@ snapshots:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-avatar@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.5)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.5
'@types/react-dom': 18.3.0
'@radix-ui/react-checkbox@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@ -3706,7 +3734,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@ -3737,7 +3765,7 @@ snapshots:
is-bun-module: 1.1.0
is-glob: 4.0.3
optionalDependencies:
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
@ -3755,7 +3783,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5

BIN
public/logo-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,24 +1,62 @@
import dynamic from "next/dynamic";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { NavBar } from "@/components/layout/nav-bar";
import { ThemeToggle } from "@/components/theme-toggle";
export default async function IndexPage() {
// 动态导入 Logo 组件以避免服务器端渲染错误
const DynamicLogo = dynamic(() => import("@/components/dynamic-logo"), {
ssr: false,
});
export default function IndexPage() {
return (
<>
<header className="flex h-24 items-center justify-center">
<div className="flex w-full max-w-5xl items-center justify-between">
<div>
<h1 className="text-xl">Next.js</h1>
<div className="flex min-h-screen flex-col bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<NavBar />
<main className="flex flex-grow items-center justify-center py-16 sm:py-24">
<div className="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
<h1 className="mb-8 bg-gradient-to-r from-[#25263A] to-[#4A4B68] bg-clip-text text-5xl font-extrabold text-transparent dark:from-[#A0A1B2] dark:to-[#D1D2E0] sm:text-6xl">
Connect
</h1>
<p className="mb-12 text-xl leading-relaxed text-gray-700 dark:text-gray-300 sm:text-2xl">
Connect Discourse SSO OAuth
2.0 Connect
便
</p>
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-6 sm:space-y-0">
<Link href="/dashboard">
<Button
size="lg"
className="transform rounded-full bg-gradient-to-r from-[#25263A] to-[#4A4B68] px-8 py-3 text-lg text-white shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-1 hover:from-[#1E1F2E] hover:to-[#3D3E56] dark:from-[#A0A1B2] dark:to-[#D1D2E0] dark:text-[#25263A] dark:hover:from-[#8A8B9C] dark:hover:to-[#BBBCCA]"
>
使
</Button>
</Link>
<Link href="https://github.com/Tuluobo/discourse-connect">
<Button
size="lg"
variant="outline"
className="transform rounded-full border-[#25263A] px-8 py-3 text-lg text-[#25263A] shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-1 hover:bg-[#25263A] hover:text-white dark:border-[#A0A1B2] dark:text-[#A0A1B2] dark:hover:bg-[#A0A1B2] dark:hover:text-[#25263A]"
>
</Button>
</Link>
</div>
<ThemeToggle />
</div>
</header>
<main className="flex flex-col items-center justify-between p-24">
<div className="z-10 flex w-full max-w-5xl flex-col items-center justify-between font-mono text-sm">
<h1>Hello, Next js & Shadcn UI & Next Auth</h1>
<br />
<Button>Start</Button>
</div>
</main>
</>
<footer className="bg-white py-8 shadow-inner dark:bg-gray-800">
<div className="mx-auto max-w-7xl px-4 text-center text-gray-600 dark:text-gray-400 sm:px-6 lg:px-8">
© 2024{" "}
<a
href="https://shuzimumin.com"
className="text-[#25263A] hover:underline dark:text-[#A0A1B2]"
>
</a>
.
</div>
</footer>
</div>
);
}

View File

@ -16,38 +16,36 @@ export const metadata: Metadata = {
export default function AuthPage({ searchParams }: Props) {
return (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<MessageCircleCode className="mx-auto size-12" />
<div className="text-2xl font-semibold tracking-tight">
<span>Welcome to</span>{" "}
<span style={{ fontFamily: "Bahamas Bold" }}></span>
</div>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<MessageCircleCode className="mx-auto size-12" />
<div className="text-2xl font-semibold tracking-tight">
<span>Welcome to</span>{" "}
<span style={{ fontFamily: "Bahamas Bold" }}></span>
</div>
<div>
<Suspense>
<UserAuthorize data={searchParams} />
</Suspense>
</div>
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="hover:text-brand underline underline-offset-4"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="hover:text-brand underline underline-offset-4"
>
Privacy Policy
</Link>
.
</p>
</div>
<div>
<Suspense>
<UserAuthorize data={searchParams} />
</Suspense>
</div>
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="hover:text-brand underline underline-offset-4"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="hover:text-brand underline underline-offset-4"
>
Privacy Policy
</Link>
.
</p>
</div>
);
}

View File

@ -14,5 +14,11 @@ export default async function AuthLayout({
redirect("/dashboard");
}
return <div className="min-h-screen">{children}</div>;
return (
<div className="min-h-screen">
<div className="flex h-screen w-screen flex-col items-center justify-center">
{children}
</div>
</div>
);
}

View File

@ -14,7 +14,7 @@ export const metadata: Metadata = {
export default function LoginPage() {
return (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<>
<Link
href="/"
className={cn(
@ -32,7 +32,9 @@ export default function LoginPage() {
<MessageCircleCode className="mx-auto size-12" />
<div className="text-2xl font-semibold tracking-tight">
<span>Welcome to</span>{" "}
<span style={{ fontFamily: "Bahamas Bold" }}></span>
<span style={{ fontFamily: "Bahamas Bold" }}>
Connect
</span>
</div>
</div>
<Suspense>
@ -56,6 +58,6 @@ export default function LoginPage() {
.
</p>
</div>
</div>
</>
);
}

View File

@ -1,19 +1,22 @@
"use client";
import * as React from "react";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { type ThemeProviderProps as ProviderProps } from "next-themes/dist/types";
export function Providers({ children, ...props }: ProviderProps) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
{children}
</ThemeProvider>
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
{...props}
>
{children}
</ThemeProvider>
</SessionProvider>
);
}

View File

@ -0,0 +1,12 @@
"use client";
import Image from "next/image";
import { useTheme } from "next-themes";
export default function DynamicLogo() {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? "/logo-dark.png" : "/logo.png";
return <Image src={logoSrc} alt="数字牧民 Logo" width={48} height={48} />;
}

View File

@ -0,0 +1,79 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { signOut, useSession } from "next-auth/react";
import DynamicLogo from "../dynamic-logo";
import { ThemeToggle } from "../theme-toggle";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
export function NavBar() {
const { data: session } = useSession();
const user = session?.user;
return (
<nav className="sticky top-0 z-10 bg-white shadow-md dark:bg-gray-800">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between py-3">
<Link href="/">
<div className="flex items-center space-x-3">
<DynamicLogo />
<h1 className="text-2xl font-bold text-[#25263A] dark:text-white">
Connect
</h1>
</div>
</Link>
<div className="flex items-center space-x-4">
<ThemeToggle />
{user ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="overflow-hidden rounded-full"
>
<Image
src={user.avatarUrl as string}
width={36}
height={36}
alt="Avatar"
className="overflow-hidden rounded-full"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Link href="/sign-in">
<Button
variant="outline"
className="border-[#25263A] text-[#25263A] hover:bg-[#25263A] hover:text-white dark:border-[#A0A1B2] dark:text-[#A0A1B2] dark:hover:bg-[#A0A1B2] dark:hover:text-[#25263A]"
>
</Button>
</Link>
)}
</div>
</div>
</div>
</nav>
);
}

View 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 };

View File

@ -1,14 +1,14 @@
import { Client } from "@prisma/client";
import { Prisma as PrismaType } from "@prisma/client";
import { prisma } from "@/lib/prisma";
export async function getClientByClientId(
clientId: string,
): Promise<Client | null> {
export async function getClientByClientId(clientId: string) {
return prisma.client.findUnique({ where: { clientId } });
}
export async function createClient(data: Omit<Client, "id">): Promise<Client> {
export async function createClient(
data: PrismaType.ClientUncheckedCreateInput,
) {
return prisma.client.create({ data });
}