+ 智能健康管家 +
++ AI 驱动的健康管理平台,提供智能对话、图片识别、卡路里计算和健康数据管理功能 +
++ {feature.title} +
++ {feature.description} +
++ 为什么选择我们 +
++ AI 智能分析 +
++ 基于先进的 AI 模型,提供精准的健康分析和建议 +
++ 简单易用 +
++ 简洁直观的界面设计,让您轻松管理健康数据 +
++ 数据安全 +
++ 严格的数据保护措施,确保您的健康信息安全 +
+{message.content}
+dAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJw b z_^v8bbg` SAn{I*4bH$u(RZ6*x UhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=p C^ S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk( $?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU ^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvh CL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c 70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397* _cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111a H}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*I cmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU &68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-= A= yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v #ix45EVrcEhr>!NMhprl $InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~ &^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7< 4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}sc Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+ 9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2 `1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M =hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S( O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..d602f8e --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,137 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --font-sans: + 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', ui-sans-serif, + system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', + 'Courier New', monospace; + --font-serif: + 'Noto Serif SC', 'Songti SC', 'SimSun', ui-serif, Georgia, Cambria, + 'Times New Roman', Times, serif; +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +body { + @apply font-sans; +} diff --git a/src/app/health/page.tsx b/src/app/health/page.tsx new file mode 100644 index 0000000..5355835 --- /dev/null +++ b/src/app/health/page.tsx @@ -0,0 +1,569 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ArrowLeft, Plus, Trash2, TrendingUp, TrendingDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import Link from 'next/link'; + +interface DietRecord { + id: string; + date: string; + food: string; + calories: number; + protein: number; + carbs: number; + fat: number; +} + +interface ExerciseRecord { + id: string; + date: string; + activity: string; + duration: number; + caloriesBurned: number; +} + +export default function HealthPage() { + const [activeTab, setActiveTab] = useState<'diet' | 'exercise' | 'stats'>('diet'); + const [dietRecords, setDietRecords] = useState ([]); + const [exerciseRecords, setExerciseRecords] = useState ([]); + + // 饮食记录表单 + const [dietForm, setDietForm] = useState({ + food: '', + calories: '', + protein: '', + carbs: '', + fat: '', + }); + + // 运动记录表单 + const [exerciseForm, setExerciseForm] = useState({ + activity: '', + duration: '', + caloriesBurned: '', + }); + + // 从 localStorage 加载数据 + useEffect(() => { + const savedDiet = localStorage.getItem('dietRecords'); + const savedExercise = localStorage.getItem('exerciseRecords'); + if (savedDiet) setDietRecords(JSON.parse(savedDiet)); + if (savedExercise) setExerciseRecords(JSON.parse(savedExercise)); + }, []); + + // 保存数据到 localStorage + useEffect(() => { + localStorage.setItem('dietRecords', JSON.stringify(dietRecords)); + }, [dietRecords]); + + useEffect(() => { + localStorage.setItem('exerciseRecords', JSON.stringify(exerciseRecords)); + }, [exerciseRecords]); + + // 添加饮食记录 + const handleAddDiet = () => { + if (!dietForm.food || !dietForm.calories) return; + + const newRecord: DietRecord = { + id: Date.now().toString(), + date: new Date().toISOString(), + food: dietForm.food, + calories: parseFloat(dietForm.calories), + protein: parseFloat(dietForm.protein) || 0, + carbs: parseFloat(dietForm.carbs) || 0, + fat: parseFloat(dietForm.fat) || 0, + }; + + setDietRecords([newRecord, ...dietRecords]); + setDietForm({ food: '', calories: '', protein: '', carbs: '', fat: '' }); + }; + + // 添加运动记录 + const handleAddExercise = () => { + if (!exerciseForm.activity || !exerciseForm.duration) return; + + const caloriesBurned = parseFloat(exerciseForm.caloriesBurned) || 0; + const newRecord: ExerciseRecord = { + id: Date.now().toString(), + date: new Date().toISOString(), + activity: exerciseForm.activity, + duration: parseFloat(exerciseForm.duration), + caloriesBurned, + }; + + setExerciseRecords([newRecord, ...exerciseRecords]); + setExerciseForm({ activity: '', duration: '', caloriesBurned: '' }); + }; + + // 删除记录 + const handleDeleteDiet = (id: string) => { + setDietRecords(dietRecords.filter((r) => r.id !== id)); + }; + + const handleDeleteExercise = (id: string) => { + setExerciseRecords(exerciseRecords.filter((r) => r.id !== id)); + }; + + // 计算统计数据 + const stats = { + totalCaloriesConsumed: dietRecords.reduce((sum, r) => sum + r.calories, 0), + totalCaloriesBurned: exerciseRecords.reduce((sum, r) => sum + r.caloriesBurned, 0), + totalProtein: dietRecords.reduce((sum, r) => sum + r.protein, 0), + totalCarbs: dietRecords.reduce((sum, r) => sum + r.carbs, 0), + totalFat: dietRecords.reduce((sum, r) => sum + r.fat, 0), + totalDuration: exerciseRecords.reduce((sum, r) => sum + r.duration, 0), + netCalories: + dietRecords.reduce((sum, r) => sum + r.calories, 0) - + exerciseRecords.reduce((sum, r) => sum + r.caloriesBurned, 0), + }; + + // 获取今日日期的记录 + const today = new Date().toDateString(); + const todayDietRecords = dietRecords.filter( + (r) => new Date(r.date).toDateString() === today + ); + const todayExerciseRecords = exerciseRecords.filter( + (r) => new Date(r.date).toDateString() === today + ); + + return ( + + {/* 导航栏 */} + + + {/* 主内容区 */} ++ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..51fe3f2 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from 'next'; +import { Inspector } from 'react-dev-inspector'; +import './globals.css'; + +export const metadata: Metadata = { + title: { + default: '智能健康管家 | AI 驱动的健康管理平台', + template: '%s | 智能健康管家', + }, + description: + '智能健康管家是一款基于 AI 的健康管理平台,提供智能对话、图片识别、卡路里计算和健康数据管理功能,帮助您轻松管理健康生活。', + keywords: [ + '健康管理', + 'AI 对话', + '图片识别', + '卡路里计算', + '营养分析', + '运动记录', + '健康数据', + '智能助手', + ], + authors: [{ name: '智能健康管家' }], + openGraph: { + title: '智能健康管家 | AI 驱动的健康管理平台', + description: 'AI 驱动的健康管理平台,提供智能对话、图片识别、卡路里计算和健康数据管理功能', + type: 'website', + }, + robots: { + index: true, + follow: true, + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const isDev = process.env.NODE_ENV === 'development'; + + return ( + + + {isDev &&+ {/* 标签页导航 */} +++ + + ++ + {/* 饮食记录标签页 */} + {activeTab === 'diet' && ( ++ {/* 添加饮食记录 */} ++ )} + + {/* 运动记录标签页 */} + {activeTab === 'exercise' && ( +++ + {/* 今日记录 */} + {todayDietRecords.length > 0 && ( ++ 添加饮食记录 +
+++ ++ + setDietForm({ ...dietForm, food: e.target.value })} + /> +++ + + setDietForm({ ...dietForm, calories: e.target.value }) + } + /> +++ + + setDietForm({ ...dietForm, protein: e.target.value }) + } + /> +++ + setDietForm({ ...dietForm, carbs: e.target.value })} + /> +++ + setDietForm({ ...dietForm, fat: e.target.value })} + /> ++++ )} + + {/* 历史记录 */} ++ 今日记录 +
++ {todayDietRecords.map((record) => ( ++++ ))} +++ ++ {record.food} +++ {record.calories} kcal | 蛋白质 {record.protein}g | 碳水{' '} + {record.carbs}g | 脂肪 {record.fat}g +++++ 历史记录 +
+ {dietRecords.length === 0 ? ( ++ 暂无记录 +
+ ) : ( ++ {dietRecords.map((record) => ( ++ )} +++ ))} +++ ++ {record.food} +++ {new Date(record.date).toLocaleString('zh-CN')} |{' '} + {record.calories} kcal +++ {/* 添加运动记录 */} ++ )} + + {/* 数据统计标签页 */} + {activeTab === 'stats' && ( +++ + {/* 今日记录 */} + {todayExerciseRecords.length > 0 && ( ++ 添加运动记录 +
+++ ++ + + setExerciseForm({ ...exerciseForm, activity: e.target.value }) + } + /> +++ + + setExerciseForm({ ...exerciseForm, duration: e.target.value }) + } + /> +++ + + setExerciseForm({ + ...exerciseForm, + caloriesBurned: e.target.value, + }) + } + /> ++++ )} + + {/* 历史记录 */} ++ 今日记录 +
++ {todayExerciseRecords.map((record) => ( ++++ ))} +++ ++ {record.activity} +++ {record.duration} 分钟 | 消耗 {record.caloriesBurned} kcal +++++ 历史记录 +
+ {exerciseRecords.length === 0 ? ( ++ 暂无记录 +
+ ) : ( ++ {exerciseRecords.map((record) => ( ++ )} +++ ))} +++ ++ {record.activity} +++ {new Date(record.date).toLocaleString('zh-CN')} |{' '} + {record.duration} 分钟 ++++ )} ++++ 健康数据统计 +
+ + {/* 卡路里统计 */} +++ + {/* 营养成分统计 */} ++ 卡路里概览 +
++++++ {stats.totalCaloriesConsumed} +++ 总摄入 (kcal) +++++ {stats.totalCaloriesBurned} +++ 总消耗 (kcal) +++++ {stats.netCalories > 0 ? ( +++ ) : ( + + )} + + {stats.netCalories > 0 ? '+' : ''} + {stats.netCalories} +++ 净卡路里 (kcal) ++++ + {/* 运动统计 */} ++ 营养成分统计 +
++++++ {stats.totalProtein}g +++ 蛋白质 +++++ {stats.totalCarbs}g +++ 碳水化合物 +++++ {stats.totalFat}g +++ 脂肪 +++++ 运动统计 +
++++++ {exerciseRecords.length} +++ 运动次数 +++++ {stats.totalDuration} +++ 总时长(分钟) ++} + {children} + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..5f50ac2 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { + MessageSquare, + Camera, + Heart, + Activity, + Utensils, + BarChart3, +} from 'lucide-react'; + +export default function Home() { + const features = [ + { + icon: MessageSquare, + title: 'AI 智能对话', + description: '与 AI 助手对话,获取专业的健康咨询和建议', + link: '/chat', + color: 'bg-blue-500', + }, + { + icon: Camera, + title: '图片识别', + description: '上传食物图片,AI 智能识别营养成分和卡路里', + link: '/recognition', + color: 'bg-green-500', + }, + { + icon: Heart, + title: '健康档案', + description: '记录和管理您的健康数据,追踪健康趋势', + link: '/health', + color: 'bg-red-500', + }, + { + icon: Activity, + title: '运动记录', + description: '记录日常运动和活动,保持健康生活方式', + link: '/health', + color: 'bg-orange-500', + }, + { + icon: Utensils, + title: '饮食管理', + description: '管理饮食记录,控制卡路里摄入', + link: '/health', + color: 'bg-purple-500', + }, + { + icon: BarChart3, + title: '数据统计', + description: '查看健康数据分析报告,了解身体状况', + link: '/health', + color: 'bg-indigo-500', + }, + ]; + + return ( + + {/* 导航栏 */} + + + {/* 主内容区 */} ++ ); +} diff --git a/src/app/recognition/page.tsx b/src/app/recognition/page.tsx new file mode 100644 index 0000000..7e94cb1 --- /dev/null +++ b/src/app/recognition/page.tsx @@ -0,0 +1,303 @@ +'use client'; + +import { useState, useRef } from 'react'; +import { Camera, Upload, X, ArrowLeft, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; + +interface NutritionInfo { + foodName: string; + calories: number; + protein: number; + carbs: number; + fat: number; + fiber: number; + description: string; +} + +export default function RecognitionPage() { + const [selectedImage, setSelectedImage] = useState+ {/* Hero 区域 */} + +++ + {/* 功能卡片网格 */} ++ 智能健康管家 +
++ AI 驱动的健康管理平台,提供智能对话、图片识别、卡路里计算和健康数据管理功能 +
++ {features.map((feature) => { + const Icon = feature.icon; + return ( + ++ + {/* 优势特点 */} ++++ + {feature.title} +
++ {feature.description} +
++ 开始使用 + ++ + ); + })} ++++ 为什么选择我们 +
++++++ AI 智能分析 +
++ 基于先进的 AI 模型,提供精准的健康分析和建议 +
++++ 简单易用 +
++ 简洁直观的界面设计,让您轻松管理健康数据 +
++++ 数据安全 +
++ 严格的数据保护措施,确保您的健康信息安全 +
+(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [nutritionInfo, setNutritionInfo] = useState (null); + const [error, setError] = useState (null); + const fileInputRef = useRef (null); + + const handleFileSelect = (event: React.ChangeEvent ) => { + const file = event.target.files?.[0]; + if (file) { + if (file.size > 5 * 1024 * 1024) { + setError('文件大小不能超过 5MB'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + setSelectedImage(e.target?.result as string); + setNutritionInfo(null); + setError(null); + }; + reader.readAsDataURL(file); + } + }; + + const handleAnalyze = async () => { + if (!selectedImage) return; + + setIsAnalyzing(true); + setError(null); + + try { + const response = await fetch('/api/analyze-food', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + image: selectedImage, + }), + }); + + if (!response.ok) { + throw new Error('分析失败,请稍后再试'); + } + + const data = await response.json(); + setNutritionInfo(data); + } catch (error) { + console.error('Analysis error:', error); + setError(error instanceof Error ? error.message : '分析失败,请稍后再试'); + } finally { + setIsAnalyzing(false); + } + }; + + const handleReset = () => { + setSelectedImage(null); + setNutritionInfo(null); + setError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( + + {/* 导航栏 */} + + + {/* 主内容区 */} ++ ); +} diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 0000000..f69940a --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,11 @@ +import { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/_next/', '/static/'], + }, + }; +} diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 0000000..4a8cca4 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps+++ {/* 左侧:上传区域 */} ++++ + {/* 右侧:分析结果 */} +++ + {/* 使用说明 */} ++ 上传食物图片 +
+ + {!selectedImage ? ( ++ + ++ ) : ( +++ )} + + {error && ( +++ + ++ +
+ {error} ++ )} ++++ 使用说明 +
++
+- • 确保图片清晰,光线充足
+- • 尽量拍摄食物的正面或侧面
+- • 避免背景过于复杂
+- • 支持单一食物或多食物组合
++++++ 营养分析结果 +
+ + {!nutritionInfo && !isAnalyzing && ( +++ )} + + {isAnalyzing && ( ++++ 上传图片后点击"开始分析"
+获取营养信息和卡路里数据
+++ )} + + {nutritionInfo && ( ++++ + AI 正在分析图片... +
++ {/* 食物名称 */} ++ )} +++ + {/* 卡路里 */} ++ {nutritionInfo.foodName} +
++ {nutritionInfo.description} +
+++ + {/* 营养成分 */} ++ + 卡路里 + + + {nutritionInfo.calories}{' '} + kcal + ++++ + {/* 保存按钮 */} + ++++ 蛋白质 +++ {nutritionInfo.protein}g +++++ 碳水化合物 +++ {nutritionInfo.carbs}g +++++ 脂肪 +++ {nutritionInfo.fat}g +++++ 膳食纤维 +++ {nutritionInfo.fiber}g ++) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps ) { + return ( + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentPropssvg]:rotate-180", + className + )} + {...props} + > + {children} + ++ ) { + return ( + + + ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps{children}+) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps ) { + return ( + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + + ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + + ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps+ + ) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps ) { + return ( + + ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + + ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..3df3fd0 --- /dev/null +++ b/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +function AspectRatio({ + ...props +}: React.ComponentProps ) { + return +} + +export { AspectRatio } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..fd3a406 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..eb88f32 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return +} + +function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { + return ( + + ) +} + +function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { + return ( + + ) +} + +function BreadcrumbLink({ + asChild, + className, + ...props +}: React.ComponentProps<"a"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot : "a" + + return ( +
+ ) +} + +function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ) +} + +function BreadcrumbSeparator({ + children, + className, + ...props +}: React.ComponentProps<"li">) { + return ( + + ) +} + +function BreadcrumbEllipsis({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, +} diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx new file mode 100644 index 0000000..8600af0 --- /dev/null +++ b/src/components/ui/button-group.tsx @@ -0,0 +1,83 @@ +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + } +) + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps ) { + return ( + + ) +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean +}) { + const Comp = asChild ? Slot : "div" + + return ( + + ) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps ) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..6a1e886 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant = 'default', + size = 'default', + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..9fb18ca --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,220 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { + DayPicker, + getDefaultClassNames, + type DayButton, +} from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps ["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( + + ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + + + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps+ {children} ++) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef (null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( +