From fae61f924e66ab1a6795548f4a580a82f5e37f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E6=97=AD?= <> Date: Tue, 30 Jun 2026 09:09:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B5=8F=E8=A7=88=E9=87=8F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E3=80=81=E6=B5=8F=E8=A7=88=E8=AE=B0=E5=BD=95=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E3=80=81UI=E8=89=B2=E5=BD=A9=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Post.viewCount 字段,文章详情/列表/精选卡片显示浏览量 - 新增 PageView 模型记录访客IP(脱敏)、设备类型、来源 - 后台新增浏览记录独立页面及侧边栏菜单 - 仪表盘新增访问概览:页面访问/今日/独立IP/设备分布 - 整体色彩体系优化:降低黑白对比度、统一暖色调 - UI细节修复:focus-visible、Footer空列、mobile hover、transition-all等10+项 --- prisma/schema.prisma | 12 +++ public/apple-touch-icon.png | Bin 0 -> 21284 bytes public/logo-full.svg | 55 +++++++++++ src/app/about/page.tsx | 2 +- src/app/admin/layout.tsx | 1 + src/app/admin/page.tsx | 116 +++++++++++++++++++++- src/app/admin/visits/page.tsx | 143 ++++++++++++++++++++++++++++ src/app/api/views/[slug]/route.ts | 16 ++++ src/app/api/visits/route.ts | 29 ++++++ src/app/api/visits/stats/route.ts | 26 +++++ src/app/favicon.ico | Bin 25931 -> 16958 bytes src/app/globals.css | 64 +++++++------ src/app/icon.png | Bin 0 -> 21284 bytes src/app/icon.svg | 44 +++++++++ src/app/layout.tsx | 3 +- src/app/posts/[slug]/page.tsx | 2 + src/components/BlogList.tsx | 7 +- src/components/Footer.tsx | 21 ---- src/components/Header.tsx | 43 +++++++-- src/components/HeroSection.tsx | 8 +- src/components/PostContent.tsx | 8 +- src/components/PostSections.tsx | 10 +- src/components/TrackingProvider.tsx | 28 ++++++ src/components/ViewTracker.tsx | 38 ++++++++ src/components/VisitTracker.tsx | 46 +++++++++ src/components/admin/PostForm.tsx | 2 +- src/data/posts.ts | 2 +- src/lib/store.ts | 138 ++++++++++++++++++++++++++- src/lib/utils.ts | 9 ++ 29 files changed, 789 insertions(+), 84 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/logo-full.svg create mode 100644 src/app/admin/visits/page.tsx create mode 100644 src/app/api/views/[slug]/route.ts create mode 100644 src/app/api/visits/route.ts create mode 100644 src/app/api/visits/stats/route.ts create mode 100644 src/app/icon.png create mode 100644 src/app/icon.svg create mode 100644 src/components/TrackingProvider.tsx create mode 100644 src/components/ViewTracker.tsx create mode 100644 src/components/VisitTracker.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 23cd012..8905972 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,7 @@ model Post { tags String // JSON 序列化的 string[] coverImage String? readingTime Int @default(5) + viewCount Int @default(0) featured Boolean @default(false) status String @default("draft") createdAt String @@ -34,3 +35,14 @@ model Tag { id String @id name String @unique } + +model PageView { + id String @id + path String + postSlug String? + ip String + userAgent String + device String @default("unknown") + referrer String? + createdAt String +} diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f12957de917863eb0e87f1772e11ad56c5d3860b GIT binary patch literal 21284 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 literal 0 HcmV?d00001 diff --git a/public/logo-full.svg b/public/logo-full.svg new file mode 100644 index 0000000..8b22997 --- /dev/null +++ b/public/logo-full.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + asui.xyz + diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 4e99a82..2218234 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -57,7 +57,7 @@ export default function AboutPage() {

你好,我是Sui diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index a7de3fe..85790df 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -10,6 +10,7 @@ const navItems = [ { label: "文章", href: "/admin/posts", icon: "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2" }, { label: "分类", href: "/admin/categories", icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }, { label: "标签", href: "/admin/tags", icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }, + { label: "浏览记录", href: "/admin/visits", icon: "M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" }, ]; export default function AdminLayout({ children }: { children: React.ReactNode }) { diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 423aa27..93038c8 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -12,11 +12,50 @@ interface Stats { featured: number; categories: number; tags: number; + totalViews: number; +} + +interface VisitRecord { + id: string; + path: string; + postSlug: string | null; + ip: string; + device: string; + referrer: string | null; + createdAt: string; +} + +interface VisitStats { + totalViews: number; + todayViews: number; + uniqueIps: number; + deviceBreakdown: { device: string; count: number }[]; +} + +const DEVICE_LABELS: Record = { + desktop: "桌面", + mobile: "手机", + tablet: "平板", + bot: "爬虫", + unknown: "未知", +}; + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const min = Math.floor(diff / 60000); + if (min < 1) return "刚刚"; + if (min < 60) return `${min} 分钟前`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr} 小时前`; + const day = Math.floor(hr / 24); + return `${day} 天前`; } export default function DashboardPage() { const [stats, setStats] = useState(null); const [recentPosts, setRecentPosts] = useState([]); + const [visitStats, setVisitStats] = useState(null); + const [recentVisits, setRecentVisits] = useState([]); const [loading, setLoading] = useState(true); const { toast } = useToast(); @@ -24,9 +63,12 @@ export default function DashboardPage() { Promise.all([ safeFetch("/api/stats", undefined, toast).then((r) => r.json()), safeFetch("/api/posts?page=1&pageSize=5&sortBy=createdAt&sortDir=desc", undefined, toast).then((r) => r.json()), - ]).then(([s, postsResult]) => { + safeFetch("/api/visits/stats?pageSize=20&stats=1", undefined, toast).then((r) => r.json()), + ]).then(([s, postsResult, visitResult]) => { setStats(s); setRecentPosts(postsResult.data ?? postsResult); + setVisitStats(visitResult.stats); + setRecentVisits(visitResult.views); setLoading(false); }).catch(() => setLoading(false)); }, [toast]); @@ -34,8 +76,9 @@ export default function DashboardPage() { if (loading) return

加载中...
; const statItems = stats ? [ - { label: "文章总数", value: stats.total, color: "text-foreground" }, + { label: "总浏览量", value: stats.totalViews.toLocaleString("zh-CN"), color: "text-accent" }, { label: "已发布", value: stats.published, color: "text-accent" }, + { label: "文章总数", value: stats.total, color: "text-foreground" }, { label: "草稿", value: stats.draft, color: "text-primary" }, { label: "精选", value: stats.featured, color: "text-primary" }, { label: "分类", value: stats.categories, color: "text-foreground" }, @@ -52,15 +95,78 @@ export default function DashboardPage() { {/* Stats */} -
+
{statItems.map((s) => ( -
+
{s.value}
-
{s.label}
+
{s.label}
))}
+ {/* Visit overview */} + {visitStats && ( +
+

访问概览

+
+
+
+ {visitStats.totalViews.toLocaleString("zh-CN")} +
+
页面访问
+
+
+
+ {visitStats.todayViews.toLocaleString("zh-CN")} +
+
今日访问
+
+
+
+ {visitStats.uniqueIps} +
+
独立 IP
+
+ {visitStats.deviceBreakdown.slice(0, 3).map((d) => ( +
+
{d.count}
+
+ {DEVICE_LABELS[d.device] || d.device} +
+
+ ))} +
+ + {/* Recent visits */} +

最近访问

+
+ {recentVisits.map((v) => ( +
+ + {DEVICE_LABELS[v.device] || v.device} + + + {v.path}{v.postSlug ? ` · ${v.postSlug}` : ""} + + + {v.ip} + + + {timeAgo(v.createdAt)} + +
+ ))} + {recentVisits.length === 0 && ( +
暂无访问记录
+ )} +
+
+ )} + {/* Recent posts */}

最近文章

diff --git a/src/app/admin/visits/page.tsx b/src/app/admin/visits/page.tsx new file mode 100644 index 0000000..088fc04 --- /dev/null +++ b/src/app/admin/visits/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useToast, safeFetch } from "@/components/Toast"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +interface VisitRecord { + id: string; + path: string; + postSlug: string | null; + ip: string; + device: string; + referrer: string | null; + createdAt: string; +} + +const DEVICE_LABELS: Record = { + desktop: "桌面", + mobile: "手机", + tablet: "平板", + bot: "爬虫", + unknown: "未知", +}; + +const DEVICE_COLORS: Record = { + desktop: "bg-accent/10 text-accent", + mobile: "bg-primary/10 text-primary", + tablet: "bg-chart-3/10 text-chart-3", + bot: "bg-muted text-muted-foreground", + unknown: "bg-muted text-muted-foreground", +}; + +export default function VisitsPage() { + const [visits, setVisits] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + + useEffect(() => { + setLoading(true); + safeFetch(`/api/visits/stats?page=${page}&pageSize=30`, undefined, toast) + .then((r) => r.json()) + .then((data) => { + setVisits(data.views); + setTotalPages(data.totalPages); + setTotal(data.total); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, [page, toast]); + + return ( +
+
+

浏览记录

+
+ 共 {total.toLocaleString("zh-CN")} 条记录 +
+
+ + {loading ? ( +
加载中...
+ ) : visits.length === 0 ? ( +
暂无浏览记录
+ ) : ( + <> +
+
+ + + + + + + + + + + + {visits.map((v) => ( + + + + + + + + ))} + +
页面设备IP来源时间
+
+ + {v.path} + + {v.postSlug && ( + 文章 + )} +
+
+ + {DEVICE_LABELS[v.device] || v.device} + + + {v.ip} + + {v.referrer || "-"} + + {new Date(v.createdAt).toLocaleString("zh-CN", { + month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", + })} +
+
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + {page} / {totalPages} + + +
+ )} + + )} +
+ ); +} diff --git a/src/app/api/views/[slug]/route.ts b/src/app/api/views/[slug]/route.ts new file mode 100644 index 0000000..a727222 --- /dev/null +++ b/src/app/api/views/[slug]/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { incrementViewCount } from "@/lib/store"; + +export async function POST( + _request: Request, + { params }: { params: Promise<{ slug: string }> } +) { + const { slug } = await params; + const count = await incrementViewCount(slug); + + if (count === null) { + return NextResponse.json({ error: "文章不存在" }, { status: 404 }); + } + + return NextResponse.json({ viewCount: count }); +} diff --git a/src/app/api/visits/route.ts b/src/app/api/visits/route.ts new file mode 100644 index 0000000..4092457 --- /dev/null +++ b/src/app/api/visits/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from "next/server"; +import { recordPageView } from "@/lib/store"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { path, postSlug, referrer } = body as { + path?: string; + postSlug?: string; + referrer?: string; + }; + + if (!path) { + return NextResponse.json({ error: "path is required" }, { status: 400 }); + } + + // 从请求头提取 IP 和 UA + const forwarded = request.headers.get("x-forwarded-for"); + const ip = forwarded?.split(",")[0]?.trim() || "127.0.0.1"; + const userAgent = request.headers.get("user-agent") || "unknown"; + const ref = referrer || request.headers.get("referer") || undefined; + + await recordPageView({ path, postSlug, ip, userAgent, referrer: ref }); + + return NextResponse.json({ ok: true }); + } catch { + return NextResponse.json({ ok: false }, { status: 500 }); + } +} diff --git a/src/app/api/visits/stats/route.ts b/src/app/api/visits/stats/route.ts new file mode 100644 index 0000000..236d131 --- /dev/null +++ b/src/app/api/visits/stats/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getPageViewsPaginated, getPageViewStats } from "@/lib/store"; +import { requireAuth } from "@/lib/http"; + +export async function GET(request: NextRequest) { + const deny = await requireAuth(); + if (deny) return deny; + + const url = new URL(request.url); + const page = parseInt(url.searchParams.get("page") || "1", 10); + const pageSize = parseInt(url.searchParams.get("pageSize") || "30", 10); + const needsStats = url.searchParams.get("stats") !== "0"; + + const [paginated, stats] = await Promise.all([ + getPageViewsPaginated(page, pageSize), + needsStats ? getPageViewStats() : Promise.resolve(null), + ]); + + return NextResponse.json({ + views: paginated.data, + total: paginated.total, + page: paginated.page, + totalPages: paginated.totalPages, + stats, + }); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..2556d93edff396b1924bee9ce9a3e425c849b7ea 100644 GIT binary patch literal 16958 zcmeI)S$9@N76#z-pXlpet8cvO-{9)s(@xzsh$146h>9a9DkzErBI1A(pv?0;kAVOQ zQzjAugb=FtQy-O_V5B9IgcZ@XJtt?Z_ucQ_wX5no*46zb{=N6!y7=$k>hAexUEN>n z>gw(Zi$isj!yM25+*?=o_kYx^cA&2A%GKWbuw!8-!_I|uhFuA}7M8|{!O|1f71k1V zIt^4<^`P zgteICRV&wzYGL-wfdwNrM&(#!z#(s;x9jl#lexRR<&&&IqcmJjCtLb8QPr7)yCv{yK z#NxPfu5*2PM|sXjZ%exeYp-L~w$|D^kO%E`+;tZ&UP|{r(KYl*dH`MHC>>qFJ%;(RwUd-?EUfhi&2UYFTA&jN% zr_Oezrm)lJy3*N}i`jq0Z>{Hpm+&pD+RX}X_cb(jq{GMB)BeM)Y4`q?w0+OHv~}0n zv}H%l?C;#y9Ov6IH|}W(op^udAmR3w-=~GOpEjcJfs1zI}mtN8sJK?Mzy;u_>)s+nAQEI+2zvKc0U4r6K+B zb3@)=zUE|FzvXn=6+9dXt{OutxW^@5j0FDDPr;9SzJ`0}-sZG<$C7&p0r}yWBKY=({6nLHHBKPrs~L+2_;Q^Y)}S-`km9nz1$GeCFj% z>8TetrYB$6kS0C1K0WsI+BE*jHEF_AYtxg{HUux*vlf0>){r)AJ)I8Lw`C2|V!=JFlriQlpICM?paN~j&UJOl$hZlmQDWUms zlUJwzJiaPD8rR7K^5W8!$8+8}cA_KKN@@%`AFlZSmH*)n{qz4Xt54?m=5zS8f7%;c za{hYd?d=)&%sIOjxu;ClHE2Xr9^$OqGBZp-fyb9Xh4@OKs^w`Qi zZ0^G!t-Kex!t>noL~uhVw)kA|&Ed3qLsRCy=}cGdhtxnL>HmUX{?o_F>3pWYqS>6o zB7W&yoXcCZJ$dSej2G?)BCp}#zY*82cir^p_cH#-C2|JtGXBVg8GrBtzdEVpKJ~V- zz-Zyu*Ju0@-}-8?Os}*Jn>_y5)9Z4a(|Y!O;4l7n-J|-qhzs#e|8l5&G3|}5S^sjt z#NbHoR0pXy_8n@?^^ZCP{!*U|NB?(Y{8#;7@M~|;_rsy@2PUk{eboJ^kIA`C*lL<8V}2JOyJ5r`U!au5A=}n z2WNxhzjORw#m{frZmSl6iP?7?PFxghG=UR5;g9dD_>2A>znT7Nf3)hLphhzThX7!)(5BzeR*ex}_evW3<>^PtU^LOS& zaK*>2huO92JmVmEHO7TsUuL}Iyj;oXK@II%e8DeI!0sL9S-sC3 zySY#633>iw1n}qi-|O?A@R#}DwVO`mm^Alk>?Q}uSNZ^4WK8gf2F#7|YpDfb6xVV+ zj>I`mGGB4rb)Kuf)_<65qtD+Jzj5c$n17>1F)0U_ z-=$sc7YmFgV1!%j&;4v)j$?IPn;(zn+RQv~neXG@8G#!iF7Rvo#0&%YoV@ z55i;p10K0r+imIBS_qxsYWP|bw%%P zi8r~45ODy%IcIaA{2&)w2eT$4KbaF=8f#qE^{jvSdsWO?TH~y(bLxooY`;^m z4#_9h!{`AnG0I=e-}1HfI(-)3F>A-xiJij>9;_Q$AHD93 zd-PP|qU47%4jXLSKSy^hYqsd9{04|7V78?ZeYj}!CbD<*W3#J zYWqE-Xd&w-u+q2OT6A8p*WS^;zF*#1+VKar)o(%U4=Ih6B9Nz=-0H_uVpQw{Q%ZX?!CVJe*o@o|C|i`4{D>XA^-pY literal 25931 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 LrJugUA?W`A8`#=m diff --git a/src/app/globals.css b/src/app/globals.css index 5ec1260..42c859b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -7,22 +7,21 @@ /* ── Design Tokens ── */ @theme inline { /* Parchment & earth palette — ink-wash tones */ - --color-parchment: #FDFCFA; - --color-parchment-deep: #F5F2EE; - --color-ink: #050404; - --color-ink-light: #121010; - --color-ink-muted: #2A2624; + --color-parchment: #F7F4EB; + --color-parchment-deep: #EDE8DC; + --color-ink: #1E1B18; + --color-ink-light: #1E1B18; + --color-ink-muted: #6B655E; --color-terracotta: #A63D2F; --color-terracotta-light: #C46B5E; --color-sage: #6E8264; - --color-sage-light: #A3B59B; - --color-warm-gray: #C5BDB4; - --color-cream: #FAF9F7; + --color-warm-gray: #CCC6BB; + --color-cream: #F0ECE2; /* Typography — 宋式 serif priority。 next/font 注入的 CSS 变量优先,回退到本地系统宋体。 */ --font-display: var(--font-noto-serif), "Cormorant Garamond", "Source Han Serif SC", "Songti SC", serif; - --font-body: var(--font-noto-serif), "Source Han Serif SC", "Songti SC", serif; + --font-body: var(--font-noto-serif), "Source Serif 4", "Source Han Serif SC", "Songti SC", serif; --font-sans: var(--font-sans); --font-mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace; @@ -93,7 +92,7 @@ body::before { position: fixed; inset: 0; pointer-events: none; - z-index: 9999; + z-index: 0; opacity: 0.03; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); } @@ -183,6 +182,11 @@ body::before { .prose-literary a:hover { text-decoration-color: var(--color-terracotta); } +.prose-literary a:focus-visible { + outline: 2px solid var(--color-terracotta); + outline-offset: 2px; + border-radius: 2px; +} .prose-literary img { border-radius: 8px; margin: 2em 0; @@ -255,34 +259,34 @@ body::before { :root { /* 水墨纸质风格 — 映射到 shadcn CSS 变量 */ - --background: #FDFCFA; /* parchment */ - --foreground: #050404; /* ink */ - --card: #FAF9F7; /* cream */ - --card-foreground: #050404; - --popover: #FAF9F7; - --popover-foreground: #050404; + --background: #F7F4EB; /* parchment */ + --foreground: #1E1B18; /* ink */ + --card: #F0ECE2; /* cream */ + --card-foreground: #1E1B18; + --popover: #F0ECE2; + --popover-foreground: #1E1B18; --primary: #A63D2F; /* terracotta */ - --primary-foreground: #FDFCFA; - --secondary: #F5F2EE; /* parchment-deep */ - --secondary-foreground: #050404; - --muted: #C5BDB4; /* warm-gray */ - --muted-foreground: #2A2624; /* ink-muted */ + --primary-foreground: #F7F4EB; + --secondary: #EDE8DC; /* parchment-deep */ + --secondary-foreground: #1E1B18; + --muted: #CCC6BB; /* warm-gray */ + --muted-foreground: #6B655E; /* ink-muted */ --accent: #6E8264; /* sage */ - --accent-foreground: #FDFCFA; - --destructive: #B91C1C; - --border: #C5BDB433; /* warm-gray/20 */ - --input: #C5BDB433; + --accent-foreground: #F7F4EB; + --destructive: #8B2C20; /* terracotta 深色变体 */ + --border: #CCC6BB33; /* warm-gray/20 */ + --input: #CCC6BB33; --ring: #A63D2F; /* terracotta */ --chart-1: #A63D2F; --chart-2: #6E8264; --chart-3: #C46B5E; - --chart-4: #A3B59B; - --chart-5: #2A2624; + --chart-4: #CCC6BB; + --chart-5: #6B655E; --radius: 0.75rem; - --sidebar: #F5F2EE; - --sidebar-foreground: #050404; + --sidebar: #EDE8DC; + --sidebar-foreground: #1E1B18; --sidebar-primary: #A63D2F; - --sidebar-primary-foreground: #FDFCFA; + --sidebar-primary-foreground: #F7F4EB; --sidebar-accent: #FAF9F7; --sidebar-accent-foreground: #050404; --sidebar-border: #C5BDB433; diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2f07fc9be9391f08be1d34406bb034a66cd5f71f GIT binary patch literal 21284 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 literal 0 HcmV?d00001 diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..e37c300 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fe58633..15874f5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; +import TrackingProvider from "@/components/TrackingProvider"; import { Noto_Serif_SC, Noto_Sans_SC, Cormorant_Garamond, Geist } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; @@ -81,7 +82,7 @@ export default function RootLayout({
- {children} + {children}