master
well 2024-11-04 16:15:49 +08:00
parent 774b52fca9
commit 8d1fee793d
37 changed files with 2408 additions and 1421 deletions

View File

@ -15,40 +15,40 @@
"dependencies": { "dependencies": {
"@3rapp/store": "workspace:*", "@3rapp/store": "workspace:*",
"@3rapp/utils": "workspace:*", "@3rapp/utils": "workspace:*",
"@ant-design/cssinjs": "^1.20.0", "@ant-design/cssinjs": "^1.21.1",
"antd": "^5.18.1", "antd": "^5.21.6",
"antd-style": "^3.6.2", "antd-style": "^3.7.1",
"axios": "^1.7.0", "axios": "^1.7.7",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.13",
"immer": "^10.1.1", "immer": "^10.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lunar-typescript": "^1.7.5", "lunar-typescript": "^1.7.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-use": "^17.5.0", "react-use": "^17.5.1",
"utility-types": "^3.11.0", "utility-types": "^3.11.0",
"zustand": "^4.5.2" "zustand": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@3rapp/core": "workspace:*", "@3rapp/core": "workspace:*",
"@3rapp/utils": "workspace:*", "@3rapp/utils": "workspace:*",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.13",
"@types/node": "^20.12.12", "@types/node": "^22.8.6",
"@types/react": "^18.3.2", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"eslint": "^8.57.0", "eslint": "^9.14.0",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-mixins": "^10.0.1", "postcss-mixins": "^11.0.3",
"postcss-nested": "^6.0.1", "postcss-nested": "^7.0.2",
"postcss-nesting": "^12.1.4", "postcss-nesting": "^13.0.1",
"prettier": "^3.2.5", "prettier": "^3.3.3",
"stylelint": "^16.5.0", "stylelint": "^16.10.0",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.14",
"typescript": "^5.4.5", "typescript": "^5.6.3",
"vite": "^5.2.11" "vite": "^5.4.10"
} }
} }

View File

@ -20,29 +20,29 @@
}, },
"dependencies": { "dependencies": {
"@3rapp/utils": "workspace:*", "@3rapp/utils": "workspace:*",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^9.1.0",
"@fastify/static": "^7.0.4", "@fastify/static": "^8.0.2",
"@nestjs/common": "^10.3.10", "@nestjs/common": "^10.4.6",
"@nestjs/core": "^10.3.10", "@nestjs/core": "^10.4.6",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.10", "@nestjs/platform-fastify": "^10.4.6",
"@nestjs/swagger": "^7.4.0", "@nestjs/swagger": "^8.0.1",
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^10.0.2",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"chalk": "4", "chalk": "4",
"chokidar": "^3.6.0", "chokidar": "^4.0.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"dayjs": "^1.11.11", "dayjs": "^1.11.13",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"fastify": "^4.28.1", "fastify": "^5.1.0",
"find-up": "5", "find-up": "5",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"meilisearch": "^0.41.0", "meilisearch": "^0.45.0",
"mysql2": "^3.11.0", "mysql2": "^3.11.3",
"ora": "5", "ora": "5",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
@ -50,46 +50,46 @@
"pm2": "^5.4.2", "pm2": "^5.4.2",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sanitize-html": "^2.13.0", "sanitize-html": "^2.13.1",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"utility-types": "^3.11.0", "utility-types": "^3.11.0",
"uuid": "^10.0.0", "uuid": "^11.0.2",
"validator": "^13.12.0", "validator": "^13.12.0",
"yaml": "^2.5.0", "yaml": "^2.6.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"devDependencies": { "devDependencies": {
"@3rapp/core": "workspace:*", "@3rapp/core": "workspace:*",
"@nestjs/cli": "^10.4.2", "@nestjs/cli": "^10.4.5",
"@nestjs/schematics": "^10.1.3", "@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.3.10", "@nestjs/testing": "^10.4.6",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.7",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.13",
"@types/node": "^20.14.12", "@types/node": "^22.8.6",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/sanitize-html": "^2.11.0", "@types/sanitize-html": "^2.13.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@types/validator": "^13.12.0", "@types/validator": "^13.12.2",
"@types/yargs": "^17.0.32", "@types/yargs": "^17.0.33",
"bun": "^1.1.21", "bun": "^1.1.33",
"bun-types": "^1.1.21", "bun-types": "^1.1.33",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.57.0", "eslint": "^9.14.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"nodemon": "^3.1.4", "nodemon": "^3.1.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.2.3", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.5.4" "typescript": "^5.6.3"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [

View File

@ -99,8 +99,8 @@ module.exports = {
'@typescript-eslint/no-unsafe-argument': 0, '@typescript-eslint/no-unsafe-argument': 0,
'@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/naming-convention': 0, '@typescript-eslint/naming-convention': 0,
"@typescript-eslint/lines-between-class-members": "error", '@typescript-eslint/lines-between-class-members': 'off',
"@typescript-eslint/no-throw-literal": "error", '@typescript-eslint/no-throw-literal': 'off',
/* ********************************** React and Hooks ********************************** */ /* ********************************** React and Hooks ********************************** */
'react/jsx-uses-react': 1, 'react/jsx-uses-react': 1,

View File

@ -15,6 +15,9 @@ const nextConfig = {
}, },
transpilePackages: ['@3rapp/store'], transpilePackages: ['@3rapp/store'],
pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
env: {
NEXTAUTH_URL: 'http://localhost:3000',
},
}; };
export default withMDX(nextConfig); export default withMDX(nextConfig);

View File

@ -115,6 +115,8 @@
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.57.1", "eslint": "^8.57.1",

View File

@ -1,11 +1,26 @@
import { notFound } from 'next/navigation'; 'use client';
import { FC } from 'react';
import { FC, useEffect, useState } from 'react';
import { PostActionForm } from '@/app/_components/post/action-form'; import { PostActionForm } from '@/app/_components/post/action-form';
import { queryPostItemByIdOrSlug } from '@/app/actions/post'; import { queryPostItemByIdOrSlug } from '@/app/actions/post';
export const PostEditForm: FC<{ id: string }> = async ({ id }) => { export const PostEditForm: FC<{ id: string }> = ({ id }) => {
const post = await queryPostItemByIdOrSlug(id); const [post, setPost] = useState(null);
if (!post) return notFound(); const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const fetchedPost = await queryPostItemByIdOrSlug(id);
setPost(fetchedPost);
setLoading(false);
};
fetchData();
}, [id]);
if (loading) return <p>...</p>;
if (!post) return <p></p>;
return <PostActionForm type="update" post={post} />; return <PostActionForm type="update" post={post} />;
}; };

View File

@ -19,9 +19,10 @@ import {
import { Input } from '@/app/_components/ui/input'; import { Input } from '@/app/_components/ui/input';
import { register } from '@/app/actions/user'; import { register } from '@/app/actions/user';
interface RegisterPageProps extends React.HTMLAttributes<HTMLDivElement> {} interface RegisterFormProps {
className?: string; // 添加 className 属性
const RegiserForm: FC<RegisterPageProps> = ({ className, ...props }) => { }
const RegiserForm: FC<RegisterFormProps> = ({ className, ...props }) => {
const form = useForm<z.infer<typeof registerSchema>>({ const form = useForm<z.infer<typeof registerSchema>>({
resolver: zodResolver(registerSchema), resolver: zodResolver(registerSchema),
mode: 'all', mode: 'all',

View File

@ -0,0 +1,5 @@
const Page = () => {
return <div>Books Page</div>;
};
export default Page;

View File

@ -5,6 +5,7 @@ import React, { PropsWithChildren, ReactNode, Suspense } from 'react';
import { Toaster } from '@/app/_components/ui/toaster'; import { Toaster } from '@/app/_components/ui/toaster';
import { auth } from '@/auth'; import { auth } from '@/auth';
import { Footer } from '../_components/footer';
import { Header } from '../_components/header'; import { Header } from '../_components/header';
import { PageSkeleton } from '../_components/loading/page'; import { PageSkeleton } from '../_components/loading/page';
@ -22,6 +23,7 @@ const AppLayout = async ({ children, modal }: PropsWithChildren & { modal: React
<div className=" tw-app-layout"> <div className=" tw-app-layout">
<Header /> <Header />
<Suspense fallback={<PageSkeleton />}>{children}</Suspense> <Suspense fallback={<PageSkeleton />}>{children}</Suspense>
<Footer />
</div> </div>
{modal} {modal}

View File

@ -0,0 +1,5 @@
const Page = () => {
return <div>Photos Page</div>;
};
export default Page;

View File

@ -0,0 +1,22 @@
import { Metadata } from 'next';
import React, { PropsWithChildren } from 'react';
import { PostSidebar } from '@/app/_components/siderbar/post-sidebar';
import { SidebarProvider } from '@/app/_components/ui/sidebar';
export const metadata: Metadata = {
title: 'about rext blog',
description:
'个人博客,提供一些ts、react、node.js、php、golang相关的技术文档以及分享一些生活琐事11111111111111111111',
keywords: 'react, next.js, web application',
};
const PostLayout = async ({ children }: PropsWithChildren) => {
return (
<SidebarProvider open={false}>
<div className=" tw-app-layout">{children}</div>
<PostSidebar />
</SidebarProvider>
);
};
export default PostLayout;

View File

@ -0,0 +1,51 @@
import { isNil } from 'lodash';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { Tools } from '@/app/_components/home/tools';
import { ItemList } from '@/app/_components/post/item/itemlist';
import { PostListPaginate } from '@/app/_components/post/paginate';
import { queryPostPaginate } from '@/app/actions/post';
const App: React.FC<{ searchParams: Record<string, any> }> = async ({ searchParams }) => {
const page =
isNil(searchParams.page) || Number(searchParams.page) < 1 ? 1 : Number(searchParams.page);
const limit = isNil(searchParams.limit) ? 10 : Number(searchParams.limit);
const category = isNil(searchParams.category) ? undefined : searchParams.category;
const { items, meta } = await queryPostPaginate({ page, limit, category });
if (meta.totalPages && meta.totalPages > 0 && page > meta.totalPages) {
return redirect('/post');
}
console.log(meta);
return (
<div className=" tw-container">
<Tools />
<div className=" tw-w-full tw-flex ">
<div className="tw-flex tw-flex-col tw-space-y-5 tw-pl-5 tw-pt-6 tw-w-1/6 tw-border ">
<Link href="/post"></Link>
<Link href="/post?category=nextjs">nextjs</Link>
<Link href="/post?category=zhihu"></Link>
</div>
<div className=" tw-flex-1 tw-ml-1">
{items.map((item) => (
<ItemList
id={item.id}
key={item.id}
title={item.title}
summary={item.summary}
body={item.body}
thumb={item.thumb}
slug={item.slug}
/>
))}
</div>
</div>
{meta.totalPages! > 1 && (
<PostListPaginate totalPages={meta.totalPages} page={page} limit={limit} />
)}
</div>
);
};
export default App;

View File

@ -8,7 +8,7 @@ type CardWrapperProps = PropsWithChildren & {
headerLabel: string; headerLabel: string;
backButtonLabel: string; backButtonLabel: string;
backButtonHref: string; backButtonHref: string;
showSocial?: boolean; // showSocial?: boolean;
}; };
export const CardWrapper: FC<CardWrapperProps> = ({ export const CardWrapper: FC<CardWrapperProps> = ({
children, children,

View File

@ -1,143 +1,205 @@
'use client'; 'use client';
import { zodResolver } from '@hookform/resolvers/zod'; import Link from 'next/link';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { FC, useState, useTransition } from 'react'; import { FC, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { login } from '@/app/actions/login'; import { login } from '@/app/actions/login';
import { LoginSchema } from '@/lib/validations/auth';
import { useToast } from '@/hooks/use-toast';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel } from '../ui/form'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
import { SuccessMessage } from './SuccessMessage '; import { Label } from '../ui/label';
import { CardWrapper } from './card-wrapper';
import { ErrorMessage } from './error-message';
const LoginForm: FC = () => { const LoginForm: FC = () => {
const [showTwoFactor, setShowTwoFactor] = useState(false); const { toast } = useToast();
const callbackUrl = useSearchParams().get('callbackUrl'); const callbackUrl = useSearchParams().get('callbackUrl');
const [error, setError] = useState<string | undefined>(''); const [email, setEmail] = useState('');
const [success, setSuccess] = useState<string | undefined>(''); const [password, setPassword] = useState('');
const [isPending, startTransition] = useTransition(); const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
const searchParams = useSearchParams(); e.preventDefault();
const urlError = login({ email, password }, callbackUrl).then((v) => {
searchParams.get('error') === 'OAuthAccountNotLinked' if (v?.error) {
? 'Email already in use with different Provider!' toast({
: ''; title: '登录信息',
const form = useForm<z.infer<typeof LoginSchema>>({ description: v.error,
mode: 'all',
defaultValues: {
email: '',
password: '',
},
resolver: zodResolver(LoginSchema),
});
const onSubmit = (data: z.infer<typeof LoginSchema>) => {
startTransition(() => {
login(data, callbackUrl)
.then((v) => {
if (v.error) {
form.reset();
setError(v.error);
}
if (v.success) {
form.reset();
setSuccess(v.success);
}
if (v.twoFactor) {
setShowTwoFactor(true);
}
})
.catch((e) => {
setError(`Something went wrong: ${e.message}`);
}); });
} else {
toast({
title: '登录成功',
description: '欢迎回来,',
});
}
}); });
}; };
return ( return (
<CardWrapper <Card className=" tw-mx-auto tw-max-w-md ">
headerLabel="欢迎登录" <CardHeader>
backButtonLabel="还没有账号?" <CardTitle className="tw-text-2xl">login</CardTitle>
backButtonHref="/auth/register" <CardDescription>Enter your email below to login to your account</CardDescription>
showSocial </CardHeader>
> <CardContent>
<Form {...form}> <div className="tw-grid tw-gap-4">
<form onSubmit={form.handleSubmit(onSubmit)}> <div className=" tw-grid tw-gap-2">
<div> <Label htmlFor="email">Email</Label>
{showTwoFactor && ( <Input
<> onChange={(e) => setEmail(e.target.value)}
<FormField id="email"
control={form.control} type="email"
name="code" placeholder="m@example.com"
render={({ field }) => ( required
<FormItem> />
<FormLabel> </FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
type="text"
placeholder="请输入验证码"
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
{!showTwoFactor && (
<>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
type="email"
placeholder="请输入邮箱"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
type="password"
placeholder="请输入密码"
disabled={isPending}
/>
</FormControl>
</FormItem>
)}
/>
</>
)}
</div> </div>
<ErrorMessage message={error || urlError} /> <div className=" tw-grid tw-gap-2">
<SuccessMessage message={success} /> <div className=" tw-flex tw-items-center">
<Button disabled={isPending} type="submit" className="w-full"> <Label>Password</Label>
{showTwoFactor ? 'Confirm' : 'Login'} <Link
className=" tw-ml-auto tw-inline-block tw-text-sm tw-underline"
href="/auth/forgot-password"
>
Forgot your password?
</Link>
</div>
<Input
onChange={(e) => setPassword(e.target.value)}
id="password"
type="password"
placeholder="Password"
required
/>
</div>
<Button className="tw-w-full" type="submit" onClick={handleSubmit}>
Login
</Button> </Button>
</form> </div>
</Form> <div className="tw-mt-4 tw-text-center tw-text-sm">
</CardWrapper> Don&apos;t have an account?{' '}
<Link className=" tw-ml-3 tw-underline" href="/auth/register">
Sign up
</Link>
</div>
</CardContent>
</Card>
); );
}; };
export default LoginForm; export default LoginForm;
// const [showTwoFactor, setShowTwoFactor] = useState(false);
// const callbackUrl = useSearchParams().get('callbackUrl');
// const [error, setError] = useState<string | undefined>('');
// const [success, setSuccess] = useState<string | undefined>('');
// const [isPending, startTransition] = useTransition();
// const searchParams = useSearchParams();
// const urlError =
// searchParams.get('error') === 'OAuthAccountNotLinked'
// ? 'Email already in use with different Provider!'
// : '';
// const form = useForm<z.infer<typeof LoginSchema>>({
// mode: 'all',
// defaultValues: {
// email: '',
// password: '',
// },
// resolver: zodResolver(LoginSchema),
// });
// const onSubmit = (data: z.infer<typeof LoginSchema>) => {
// startTransition(() => {
// login(data, callbackUrl)
// .then((v) => {
// if (v.error) {
// form.reset();
// setError(v.error);
// }
// if (v.success) {
// form.reset();
// setSuccess(v.success);
// }
// if (v.twoFactor) {
// setShowTwoFactor(true);
// }
// })
// .catch((e) => {
// setError(`Something went wrong: ${e.message}`);
// });
// });
// };
// <CardWrapper
// headerLabel="欢迎登录"
// backButtonLabel="还没有账号?"
// backButtonHref="/auth/register"
// >
// <Form {...form}>
// <form onSubmit={form.handleSubmit(onSubmit)}>
// <div>
// {showTwoFactor && (
// <>
// <FormField
// control={form.control}
// name="code"
// render={({ field }) => (
// <FormItem>
// <FormLabel> 验证码 </FormLabel>
// <FormControl>
// <Input
// {...field}
// disabled={isPending}
// type="text"
// placeholder="请输入验证码"
// />
// </FormControl>
// </FormItem>
// )}
// />
// </>
// )}
// {!showTwoFactor && (
// <>
// <FormField
// control={form.control}
// name="email"
// render={({ field }) => (
// <FormItem>
// <FormLabel>邮箱</FormLabel>
// <FormControl>
// <Input
// {...field}
// disabled={isPending}
// type="email"
// placeholder="请输入邮箱"
// />
// </FormControl>
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="password"
// render={({ field }) => (
// <FormItem>
// <FormLabel>密码</FormLabel>
// <FormControl>
// <Input
// {...field}
// type="password"
// placeholder="请输入密码"
// disabled={isPending}
// />
// </FormControl>
// </FormItem>
// )}
// />
// </>
// )}
// </div>
// <ErrorMessage message={error || urlError} />
// <SuccessMessage message={success} />
// <Button disabled={isPending} type="submit" className="w-full">
// {showTwoFactor ? 'Confirm' : 'Login'}
// </Button>
// </form>
// </Form>
// </CardWrapper>

View File

@ -0,0 +1,20 @@
export const Footer = () => {
return (
<footer className=" tw-my-6 md:tw-px-8 md:tw-py-0 tw-border-t-2 tw-border-dotted tw-border-blue-950">
<div className=" tw-container tw-flex tw-flex-col tw-items-center tw-justify-center">
<p className=" tw-text-center tw-text-balance tw-text-muted-foreground tw-leading-loose ">
访{' '}
<a
target="_blank"
rel="noreferrer"
href="https://github.com/lee-pham/web2"
className=" tw-font-medium tw-underline tw-underline-offset-4"
>
GitHub
</a>
<span></span>
</p>
</div>
</footer>
);
};

View File

@ -1,8 +1,11 @@
import { HomeIcon } from '@radix-ui/react-icons'; 'use client';
import { BookmarkIcon, CameraIcon, CrumpledPaperIcon, HomeIcon } from '@radix-ui/react-icons';
import localFont from 'next/font/local'; import localFont from 'next/font/local';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Nav } from './nav';
import { User } from './user'; import { User } from './user';
const myFont = localFont({ const myFont = localFont({
@ -15,9 +18,19 @@ export const Header = () => {
<div className=" tw-w-40"> <div className=" tw-w-40">
<p className={cn(myFont.className, ' tw-text-4xl tw-text-gray-800')}></p> <p className={cn(myFont.className, ' tw-text-4xl tw-text-gray-800')}></p>
</div> </div>
<div className=" tw-rounded-full tw-h-14 tw-w-14 tw-flex tw-items-center tw-justify-center tw-flex-col tw-bg-violet-500 tw-text-white "> <div className=" tw-flex tw-space-x-4">
<HomeIcon className=" tw-h-6 tw-w-6" /> <Nav title="首页" links="/">
<p className="tw-text-sm tw-font-bold"></p> <HomeIcon className=" tw-h-6 tw-w-6 tw-mx-auto" />
</Nav>
<Nav title="文章" links="/post">
<CrumpledPaperIcon className=" tw-h-6 tw-w-6 tw-mx-auto" />
</Nav>
<Nav title="书籍" links="/books">
<BookmarkIcon className=" tw-h-6 tw-w-6 tw-mx-auto" />
</Nav>
<Nav title="图片" links="/photos">
<CameraIcon className=" tw-h-6 tw-w-6 tw-mx-auto" />
</Nav>
</div> </div>
<div className="tw-flex-1"> </div> <div className="tw-flex-1"> </div>
<div className="tw-w-48"></div> <div className="tw-w-48"></div>

View File

@ -0,0 +1,27 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { FC, PropsWithChildren } from 'react';
import { cn } from '@/lib/utils';
export const Nav: FC<PropsWithChildren & { title: string; links: string }> = ({
title,
links,
children,
}) => {
const pathname = usePathname();
const isActive = pathname === links;
return (
<div
className={cn(
' tw-rounded-full tw-h-14 tw-w-14 tw-flex tw-items-center tw-justify-center tw-flex-col tw-text-white ',
isActive && 'tw-bg-violet-500',
)}
>
<Link href={links}>
{children} <p className="tw-text-sm tw-font-bold">{title}</p>
</Link>
</div>
);
};

View File

@ -0,0 +1,49 @@
import Link from 'next/link';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Input } from './ui/input';
import { Label } from './ui/label';
export function LoginForm() {
return (
<Card className="tw-mx-auto tw-max-w-sm">
<CardHeader>
<CardTitle className="tw-text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<div className="tw-grid tw-gap-4">
<div className="tw-grid tw-gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required />
</div>
<div className="tw-grid tw-gap-2">
<div className="tw-flex tw-items-center">
<Label htmlFor="password">Password</Label>
<Link
href="#"
className="tw-ml-auto tw-inline-block tw-text-sm tw-underline"
>
Forgot your password?
</Link>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="tw-w-full">
Login
</Button>
<Button variant="outline" className="tw-w-full">
Login with Google
</Button>
</div>
<div className="tw-mt-4 tw-text-center tw-text-sm">
Don&apos;t have an account?{' '}
<Link href="#" className="tw-underline">
Sign up
</Link>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,68 @@
import { isNil } from 'lodash';
import Image from 'next/image';
import Link from 'next/link';
import { AiOutlineCalendar } from 'react-icons/ai';
import { PostDelete } from '../delete';
import { PostEditButton } from '../edit-button';
type Props = {
id: string;
title: string;
summary: string;
body: string;
thumb: string;
slug: string;
};
export const ItemList = (item: Props) => {
return (
<div
key={item.id}
className=" hover:before:tw-opacity-100
last:tw-mb-0 tw-rounded-md tw-mb-8 tw-flex tw-flex-col tw-backdrop-blur-md tw-duration-300 tw-drop-shadow-[5px_5px_5px_rgba(0,0,0,0.35)]"
>
<Link
href={`/post/${item.id}`}
className="tw-relative tw-w-full tw-h-36 md:tw-h-48 lg:tw-h-72 tw-block"
>
<Image
className=" tw-rounded-tl-md tw-rounded-tr-md tw-opacity-60"
src={item.thumb}
fill
alt=""
unoptimized
priority
sizes="100%"
/>
</Link>
<div className=" hover:tw-bg-white tw-w-full tw-bg-zinc-100/80 tw-rounded-bl-md tw-rounded-br-md tw-px-5">
<div className=" tw-py-3">
<Link
className=" tw-w-full tw-block tw-overflow-hidden"
href={`/post/${item.slug ? item.slug : item.id}`}
>
<h2 className=" tw-text-lg tw-font-bold tw-ellips tw-animate-decoration tw-animate-decoration-lg">
{item.title}
</h2>
</Link>
</div>
<div className=" tw-py-3 tw-text-sm">
{isNil(item.summary) ? item.body.substring(0, 99) : item.summary}
</div>
<div className=" tw-py-3 tw-flex tw-justify-between">
<div className=" tw-flex tw-items-center">
<span>
<AiOutlineCalendar />
</span>
<time dateTime="2024-08-10" />
</div>
<div className="flex tw-items-center">
<PostEditButton id={item.id} />
{/* 删除按钮 */}
<PostDelete id={item.id} />
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,63 @@
import { Calendar, Home, Inbox, Search, Settings } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
} from '../ui/sidebar';
const items = [
{
title: 'Home',
url: '#',
icon: Home,
},
{
title: 'Inbox',
url: '#',
icon: Inbox,
},
{
title: 'Calendar',
url: '#',
icon: Calendar,
},
{
title: 'Search',
url: '#',
icon: Search,
},
{
title: 'Settings',
url: '#',
icon: Settings,
},
];
export const PostSidebar = () => {
return (
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
};

View File

@ -23,9 +23,7 @@ const Command = React.forwardRef<
)); ));
Command.displayName = CommandPrimitive.displayName; Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {} const CommandDialog = ({ children, ...props }: DialogProps) => {
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="tw-overflow-hidden tw-p-0"> <DialogContent className="tw-overflow-hidden tw-p-0">

View File

@ -2,9 +2,7 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
return ( return (
<input <input

View File

@ -2,22 +2,21 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} const Textarea = React.forwardRef<
HTMLTextAreaElement,
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( React.TextareaHTMLAttributes<HTMLTextAreaElement>
({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
className={cn( className={cn(
'tw-flex tw-min-h-[60px] tw-w-full tw-rounded-md tw-border tw-border-input tw-bg-transparent tw-px-3 tw-py-2 tw-text-sm tw-shadow-sm placeholder:tw-text-muted-foreground focus-visible:tw-outline-none focus-visible:tw-ring-1 focus-visible:tw-ring-ring disabled:tw-cursor-not-allowed disabled:tw-opacity-50', 'tw-flex tw-min-h-[60px] tw-w-full tw-rounded-md tw-border tw-border-input tw-bg-transparent tw-px-3 tw-py-2 tw-text-sm tw-shadow-sm placeholder:tw-text-muted-foreground focus-visible:tw-outline-none focus-visible:tw-ring-1 focus-visible:tw-ring-ring disabled:tw-cursor-not-allowed disabled:tw-opacity-50',
className, className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
); );
}, });
);
Textarea.displayName = 'Textarea'; Textarea.displayName = 'Textarea';
export { Textarea }; export { Textarea };

View File

@ -17,7 +17,7 @@ import { DEFAULT_LOGIN_REDIRECT } from '@/routes';
import { generateTwoFactorToken, generateVerificationToken } from './token'; import { generateTwoFactorToken, generateVerificationToken } from './token';
// 处理用户登录的异步函数,验证用户凭据并实现双因素认证 // 处理用户登录的异步函数,验证用户凭据并实现双因素认证
export const login = async (values: z.infer<typeof LoginSchema>, callbackUrl: string | null) => { export const login = async (values: z.infer<typeof LoginSchema>, callbackUrl?: string | null) => {
const validatedFields = await LoginSchema.safeParseAsync(values); const validatedFields = await LoginSchema.safeParseAsync(values);
if (!validatedFields.success) return { error: '登录参数错误!' }; if (!validatedFields.success) return { error: '登录参数错误!' };
const { email, password, code } = validatedFields.data; const { email, password, code } = validatedFields.data;

View File

@ -19,6 +19,18 @@ export const queryPostPaginate = async (
options?: PaginateOptions, options?: PaginateOptions,
): Promise<PaginateReturn<Post>> => { ): Promise<PaginateReturn<Post>> => {
// 此处使用倒序,以便新增的文章可以排在最前面 // 此处使用倒序,以便新增的文章可以排在最前面
console.log('queryPostPaginate', !isNil(options.category));
if (options.category) {
const posts = await db.$extends(paginateExt).post.paginate({
where: { keywords: { contains: options.category } },
orderBy: { updatedAt: 'desc' },
page: 1,
limit: 8,
...options,
});
return paginateTransform(posts);
}
const posts = await db.$extends(paginateExt).post.paginate({ const posts = await db.$extends(paginateExt).post.paginate({
orderBy: { updatedAt: 'desc' }, orderBy: { updatedAt: 'desc' },
page: 1, page: 1,

View File

@ -29,7 +29,8 @@ export const getVerificationTokenByEmail = async (email: string) => {
try { try {
const token = await db.verificationToken.findFirst({ where: { email } }); const token = await db.verificationToken.findFirst({ where: { email } });
return token; return token;
} catch (error) { } catch (e) {
console.log(e);
return null; return null;
} }
}; };

View File

@ -0,0 +1,9 @@
import { LoginForm } from '../_components/login-form';
export default function Page() {
return (
<div className="tw-flex tw-h-screen tw-w-full tw-items-center tw-justify-center tw-px-4">
<LoginForm />
</div>
);
}

View File

@ -1,5 +1,4 @@
body { body {
/* 设置全局背景图片 */ /* 设置全局背景图片 tw-bg-[url(images/bg.png)] */
@apply tw-bg-[url(images/bg.png)] @apply tw-bg-fixed tw-bg-cover tw-bg-no-repeat tw-bg-center tw-bg-slate-100;
tw-bg-fixed tw-bg-cover tw-bg-no-repeat tw-bg-center;
} }

View File

@ -101,6 +101,7 @@ const config = {
}, },
}, },
}, },
// eslint-disable-next-line @typescript-eslint/no-require-imports
plugins: [require('tailwindcss-animate')], plugins: [require('tailwindcss-animate')],
} satisfies Config; } satisfies Config;

View File

@ -15,6 +15,7 @@ export const {
signIn, signIn,
signOut, signOut,
} = NextAuth({ } = NextAuth({
trustHost: true,
pages: { pages: {
signIn: '/auth/login', signIn: '/auth/login',
error: '/auth/error', error: '/auth/error',

View File

@ -15,13 +15,13 @@ type ToasterToast = ToastProps & {
action?: ToastActionElement; action?: ToastActionElement;
}; };
const actionTypes = { // const actionTypes = {
ADD_TOAST: 'ADD_TOAST', // ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST', // UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST', // DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST', // REMOVE_TOAST: 'REMOVE_TOAST',
} as const; // } as const;
// type ActionType = typeof actionTypes;
let count = 0; let count = 0;
function genId() { function genId() {
@ -29,7 +29,12 @@ function genId() {
return count.toString(); return count.toString();
} }
type ActionType = typeof actionTypes; interface ActionType {
readonly ADD_TOAST: 'ADD_TOAST';
readonly UPDATE_TOAST: 'UPDATE_TOAST';
readonly DISMISS_TOAST: 'DISMISS_TOAST';
readonly REMOVE_TOAST: 'REMOVE_TOAST';
}
type Action = type Action =
| { | {

View File

@ -36,6 +36,10 @@ export interface PaginateOptions {
* *
*/ */
limit?: number; limit?: number;
/**
*
*/
category?: string;
} }
/** /**

View File

@ -28,7 +28,7 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"turbo": "^2.2.3", "turbo": "^2.2.3",
"typescript": "^5.5.4" "typescript": "^5.6.3"
}, },
"packageManager": "pnpm@9.7.0", "packageManager": "pnpm@9.7.0",
"engines": { "engines": {

View File

@ -15,27 +15,28 @@
"lint": "eslint \"**/*.ts\" --fix" "lint": "eslint \"**/*.ts\" --fix"
}, },
"devDependencies": { "devDependencies": {
"@next/eslint-plugin-next": "^14.2.5", "@next/eslint-plugin-next": "^15.0.2",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.12.2",
"eslint": "^8.57.0", "@typescript-eslint/parser": "^8.12.2",
"eslint": "^9.14.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^2.0.12", "eslint-config-turbo": "^2.2.3",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.0", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.9", "eslint-plugin-react-refresh": "^0.4.14",
"eslint-plugin-unused-imports": "^4.0.1", "eslint-plugin-unused-imports": "^4.1.4",
"jest": "^29.7.0", "jest": "^29.7.0",
"stylelint": "^16.8.1", "stylelint": "^16.10.0",
"stylelint-config-css-modules": "^4.4.0", "stylelint-config-css-modules": "^4.4.0",
"stylelint-config-recess-order": "^5.0.1", "stylelint-config-recess-order": "^5.1.1",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2", "stylelint-prettier": "^5.0.2",
"typescript": "^5.5.4" "typescript": "^5.6.3"
} }
} }

View File

@ -14,15 +14,15 @@
}, },
"devDependencies": { "devDependencies": {
"@3rapp/core": "workspace:*", "@3rapp/core": "workspace:*",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.13",
"@types/node": "^20.12.12", "@types/node": "^22.8.6",
"eslint": "^8.57.0", "eslint": "^9.14.0",
"prettier": "^3.2.5", "prettier": "^3.3.3",
"typescript": "^5.4.5" "typescript": "^5.6.3"
}, },
"dependencies": { "dependencies": {
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.3.1", "react": "^18.3.1",
"zustand": "^4.5.2" "zustand": "^5.0.1"
} }
} }

View File

@ -25,11 +25,11 @@
}, },
"devDependencies": { "devDependencies": {
"@3rapp/core": "workspace:*", "@3rapp/core": "workspace:*",
"@types/node": "^20.12.12", "@types/node": "^22.8.6",
"bunchee": "^5.1.5", "bunchee": "^5.6.1",
"eslint": "^8.57.0", "eslint": "^9.14.0",
"prettier": "^3.2.5", "prettier": "^3.3.3",
"typescript": "^5.4.5" "typescript": "^5.6.3"
}, },
"dependencies": { "dependencies": {
"deepmerge": "^4.3.1" "deepmerge": "^4.3.1"

File diff suppressed because it is too large Load Diff