1104u
parent
774b52fca9
commit
8d1fee793d
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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} />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Books Page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
const Page = () => {
|
||||||
|
return <div>Photos Page</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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>) => {
|
} else {
|
||||||
startTransition(() => {
|
toast({
|
||||||
login(data, callbackUrl)
|
title: '登录成功',
|
||||||
.then((v) => {
|
description: '欢迎回来,',
|
||||||
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}`);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
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 && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="code"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel> 验证码 </FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
disabled={isPending}
|
id="email"
|
||||||
type="text"
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!showTwoFactor && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>邮箱</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
disabled={isPending}
|
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="请输入邮箱"
|
placeholder="m@example.com"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</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'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>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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't have an account?{' '}
|
||||||
|
<Link href="#" className="tw-underline">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,10 +2,10 @@ 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(
|
||||||
|
@ -16,8 +16,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
Textarea.displayName = 'Textarea';
|
Textarea.displayName = 'Textarea';
|
||||||
|
|
||||||
export { Textarea };
|
export { Textarea };
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 =
|
||||||
| {
|
| {
|
||||||
|
|
|
@ -36,6 +36,10 @@ export interface PaginateOptions {
|
||||||
* 每页显示数量
|
* 每页显示数量
|
||||||
*/
|
*/
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
/**
|
||||||
|
* 分类字段
|
||||||
|
*/
|
||||||
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
2895
pnpm-lock.yaml
2895
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue