diff --git a/.env b/.env new file mode 100644 index 0000000..bcde272 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +DATA_PATH=data +STATIC_FILE_PATH=$DATA_PATH/static +CARD_FILE_PATH=$DATA_PATH/card diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..65d11c1 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +MARKCARD_TOKEN=admin diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..e69de29 diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 5ef6a52..d8127f9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env*.local # vercel .vercel @@ -39,3 +39,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# dev project +/.vscode/ +data/* +!data/.gitkeep diff --git a/app/api/card/[[...file]]/route.ts b/app/api/card/[[...file]]/route.ts new file mode 100644 index 0000000..424de6a --- /dev/null +++ b/app/api/card/[[...file]]/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server' +import { MarkCardI, addCard, updateCard, getCards, getPath } from '@/lib/markcard/card' +import { notFound } from 'next/navigation' + +async function getFilePath(params: Promise<{ file?: string[] }> ): Promise { + const file = (await params).file + // console.log('file', file) + // const files = file ? file[0].split(',') : undefined + const filepath = await getPath(undefined, file) + if (filepath === null) { + return notFound() + } + return filepath +} + +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ file: string[] }> } +) { + const filePath = await getFilePath(params) + const cards = await getCards(filePath) + + if (cards === undefined) { + return notFound() + } else { + return NextResponse.json(cards, { status: 200 }) + } +} + +export async function POST(req: NextRequest, + { params }: { params: Promise<{ file?: string[] }> } +) { + const card: MarkCardI = await req.json() + if (!card) { + return NextResponse.json({ error: 'Invalid card data' }, { status: 400 }) + } + + try { + const newcard = await addCard(card, await getFilePath(params)) + return NextResponse.json(newcard, { status: 200 }) + } catch (error) { + return NextResponse.json({ error }, { status: 400 }) + } +} + +export async function PATCH(req: NextRequest, + { params }: { params: Promise<{ file?: string[] }> } +) { + const card: MarkCardI = await req.json() + if (!card) { + return NextResponse.json({ error: 'Invalid card data' }, { status: 400 }) + } + + const retcard = await updateCard(card, card.id, await getFilePath(params)) + return NextResponse.json(retcard, { status: 200 }) +} diff --git a/app/api/card/validate/route.ts b/app/api/card/validate/route.ts new file mode 100644 index 0000000..470c42c --- /dev/null +++ b/app/api/card/validate/route.ts @@ -0,0 +1,12 @@ +import env from '@/lib/env' +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(req: NextRequest) { + const { token, userId } = await req.json() + if (userId === undefined) { + if (token === env('MARKCARD_TOKEN')) { + return NextResponse.json({ validate: true }, { status: 200 }) + } + } + return NextResponse.json({ validate: false }, { status: 401 }) +} diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..f683c56 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css index 6b717ad..942f871 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,20 +2,71 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; +body { + font-family: Arial, Helvetica, sans-serif; } -@media (prefers-color-scheme: dark) { +@layer base { :root { - --background: #0a0a0a; - --foreground: #ededed; + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..f1e0828 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,17 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +// import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import AlertWeb from "@/components/AlertWeb"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); +// const geistSans = Geist({ +// variable: "--font-geist-sans", +// subsets: ["latin"], +// }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +// const geistMono = Geist_Mono({ +// variable: "--font-geist-mono", +// subsets: ["latin"], +// }); export const metadata: Metadata = { title: "Create Next App", @@ -25,8 +26,9 @@ export default function RootLayout({ return ( + {children} diff --git a/app/markcard/[[...url]]/page.tsx b/app/markcard/[[...url]]/page.tsx new file mode 100644 index 0000000..6f1efe6 --- /dev/null +++ b/app/markcard/[[...url]]/page.tsx @@ -0,0 +1,21 @@ +import React, { Suspense } from 'react' +import MarkCards from '@/components/markcard/MarkCardsClient' + +export default async function page({ + params, +}: { + params: Promise<{ url?: string[] }> +}) { + const url = (await params).url + + return ( +
+
MarkCards
+
+ loading...

}> + +
+
+
+ ) +} diff --git a/app/next/page.tsx b/app/next/page.tsx new file mode 100644 index 0000000..9007252 --- /dev/null +++ b/app/next/page.tsx @@ -0,0 +1,101 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{" "} + + app/page.tsx + + . +
  2. +
  3. Save and see your changes instantly.
  4. +
+ +
+ + Vercel logomark + Deploy now + + + Read our docs + +
+
+ +
+ ); +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..7dda44a --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,20 @@ +import { Button } from '@/components/ui/button' +import Link from 'next/link' + +export default function NotFound() { + return ( +
+
404
+

Not Found

+

Could not find requested resource

+ +
+ //

404

This page could not be found.

+ ) +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 9007252..ad5b2a4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,101 +1,15 @@ -import Image from "next/image"; +import MarkCards from '@/components/markcard/MarkCardsServer' +import React from 'react' +import path from 'path' +import { redirect } from 'next/navigation' -export default function Home() { +export default function page() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- -
- ); + redirect('/markcard') + //
+ //
+ // + //
+ //
+ ) } diff --git a/app/static/[...path]/route.ts b/app/static/[...path]/route.ts new file mode 100644 index 0000000..51654c7 --- /dev/null +++ b/app/static/[...path]/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from 'next/server' +import fs from 'fs/promises' +import path from 'path' +import env from '@/lib/env'; +import resolveFilePath from '@/lib/file'; + +// 白名单:允许访问的文件扩展名 +const allowedExtensions = ['.html', '.js', '.css', '.json', '.png', '.jpg', '.jpeg', '.gif', '.svg']; + +const basePath = path.join(process.cwd(), env('STATIC_FILE_PATH')) + +export async function GET(req: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const fileseg = (await params).path + + const filepath = resolveFilePath(fileseg, basePath, allowedExtensions, ['index.html']) + + if (filepath === null) { + return NextResponse.json({ message: 'File not found' }, { status: 404 }) + } + + try { + // 获取文件扩展名以确定 MIME 类型 + const extname = path.extname(filepath).toLowerCase(); + if (!allowedExtensions.includes(extname)) { + return NextResponse.json({ error: 'Forbidden file type' }, { status: 403 }); + } + // 设置响应头 + let contentType = 'text/plain'; + switch (extname) { + case '.html': + contentType = 'text/html'; + break; + case '.js': + contentType = 'text/javascript'; + break; + case '.css': + contentType = 'text/css'; + break; + case '.json': + contentType = 'application/json'; + break; + case '.png': + contentType = 'image/png'; + break; + case '.jpg': + case '.jpeg': + contentType = 'image/jpeg'; + break; + case '.gif': + contentType = 'image/gif'; + break; + case '.svg': + contentType = 'image/svg+xml'; + break; + default: + contentType = 'text/plain'; + } + const responseHeaders = new Headers(); + responseHeaders.set('Content-Type', contentType); + + // 返回文件内容 + return new NextResponse(await fs.readFile(filepath), { status: 200, headers: responseHeaders }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..a312865 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/AlertWeb.tsx b/components/AlertWeb.tsx new file mode 100644 index 0000000..b42eaf9 --- /dev/null +++ b/components/AlertWeb.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { AlertCircle } from "lucide-react" + +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert" + +export default function AlertWeb() { + return ( +
+ + + 注意 Waring + + 该网站仅供个人学习使用 + This website is for personal learning purposes only + + +
+ ) +} diff --git a/components/markcard/EditMarkCardModal.tsx b/components/markcard/EditMarkCardModal.tsx new file mode 100644 index 0000000..0d19eb6 --- /dev/null +++ b/components/markcard/EditMarkCardModal.tsx @@ -0,0 +1,136 @@ +'use client' + +import { useState } from 'react' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { MarkCardI } from '@/lib/markcard/card' + +interface EditCardModalProps { + isOpen: boolean + onClose: () => void + cardData: MarkCardI + onSave: (updatedData: Partial) => void +} + +export default function EditCardModal({ isOpen, onClose, cardData, onSave }: EditCardModalProps) { + const [editedData, setEditedData] = useState(cardData) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setEditedData((prev) => ({ ...prev, [name]: value })) + } + + const handleLinkChange = (index: number, field: 'title' | 'url', value: string) => { + setEditedData((prev) => ({ + ...prev, + links: prev.links.map((link, i) => (i === index ? { ...link, [field]: value } : link)), + })) + } + + const handleAddLink = () => { + setEditedData((prev) => ({ + ...prev, + links: [...prev.links, { title: '', url: '' }], + })) + } + + const handleRemoveLink = (index: number) => { + setEditedData((prev) => ({ + ...prev, + links: prev.links.filter((_, i) => i !== index), + })) + } + + const handleSave = () => { + onSave(editedData) + } + + return ( + + + + Edit Card + + Here you can modify the card details and links. Click `Save changes` after you are done. + + +
+
+ + +
+
+ + +
+
+ +