mirror of
https://github.com/escalante29/healthy-fit.git
synced 2026-03-21 12:28:46 +01:00
Migrate frontend to TypeScript and extend user profile
Converted frontend codebase from JavaScript to TypeScript, including pages, components, and context. Added new layout and UI kit components. Updated backend user model and schemas to support profile fields (firstname, lastname, age, gender, height, weight, unit_preference) and added endpoints for reading/updating current user. Introduced food log listing endpoint and migration script for user table. Updated dependencies and build configs for TypeScript and Tailwind v4.
This commit is contained in:
157
frontend/src/components/catalyst/checkbox.tsx
Normal file
157
frontend/src/components/catalyst/checkbox.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import * as Headless from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import type React from 'react'
|
||||
|
||||
export function CheckboxGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="control"
|
||||
{...props}
|
||||
className={clsx(
|
||||
className,
|
||||
// Basic groups
|
||||
'space-y-3',
|
||||
// With descriptions
|
||||
'has-data-[slot=description]:space-y-6 has-data-[slot=description]:**:data-[slot=label]:font-medium'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CheckboxField({
|
||||
className,
|
||||
...props
|
||||
}: { className?: string } & Omit<Headless.FieldProps, 'as' | 'className'>) {
|
||||
return (
|
||||
<Headless.Field
|
||||
data-slot="field"
|
||||
{...props}
|
||||
className={clsx(
|
||||
className,
|
||||
// Base layout
|
||||
'grid grid-cols-[1.125rem_1fr] gap-x-4 gap-y-1 sm:grid-cols-[1rem_1fr]',
|
||||
// Control layout
|
||||
'*:data-[slot=control]:col-start-1 *:data-[slot=control]:row-start-1 *:data-[slot=control]:mt-0.75 sm:*:data-[slot=control]:mt-1',
|
||||
// Label layout
|
||||
'*:data-[slot=label]:col-start-2 *:data-[slot=label]:row-start-1',
|
||||
// Description layout
|
||||
'*:data-[slot=description]:col-start-2 *:data-[slot=description]:row-start-2',
|
||||
// With description
|
||||
'has-data-[slot=description]:**:data-[slot=label]:font-medium'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const base = [
|
||||
// Basic layout
|
||||
'relative isolate flex size-4.5 items-center justify-center rounded-[0.3125rem] sm:size-4',
|
||||
// Background color + shadow applied to inset pseudo element, so shadow blends with border in light mode
|
||||
'before:absolute before:inset-0 before:-z-10 before:rounded-[calc(0.3125rem-1px)] before:bg-white before:shadow-sm',
|
||||
// Background color when checked
|
||||
'group-data-checked:before:bg-(--checkbox-checked-bg)',
|
||||
// Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo
|
||||
'dark:before:hidden',
|
||||
// Background color applied to control in dark mode
|
||||
'dark:bg-white/5 dark:group-data-checked:bg-(--checkbox-checked-bg)',
|
||||
// Border
|
||||
'border border-zinc-950/15 group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent group-data-hover:border-zinc-950/30 group-data-checked:bg-(--checkbox-checked-border)',
|
||||
'dark:border-white/15 dark:group-data-checked:border-white/5 dark:group-data-hover:group-data-checked:border-white/5 dark:group-data-hover:border-white/30',
|
||||
// Inner highlight shadow
|
||||
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)] after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
|
||||
'dark:after:-inset-px dark:after:hidden dark:after:rounded-[0.3125rem] dark:group-data-checked:after:block',
|
||||
// Focus ring
|
||||
'group-data-focus:outline-2 group-data-focus:outline-offset-2 group-data-focus:outline-blue-500',
|
||||
// Disabled state
|
||||
'group-data-disabled:opacity-50',
|
||||
'group-data-disabled:border-zinc-950/25 group-data-disabled:bg-zinc-950/5 group-data-disabled:[--checkbox-check:var(--color-zinc-950)]/50 group-data-disabled:before:bg-transparent',
|
||||
'dark:group-data-disabled:border-white/20 dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:[--checkbox-check:var(--color-white)]/50 dark:group-data-checked:group-data-disabled:after:hidden',
|
||||
// Forced colors mode
|
||||
'forced-colors:[--checkbox-check:HighlightText] forced-colors:[--checkbox-checked-bg:Highlight] forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
|
||||
'dark:forced-colors:[--checkbox-check:HighlightText] dark:forced-colors:[--checkbox-checked-bg:Highlight] dark:forced-colors:group-data-disabled:[--checkbox-check:Highlight]',
|
||||
]
|
||||
|
||||
const colors = {
|
||||
'dark/zinc': [
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
|
||||
'dark:[--checkbox-checked-bg:var(--color-zinc-600)]',
|
||||
],
|
||||
'dark/white': [
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
|
||||
'dark:[--checkbox-check:var(--color-zinc-900)] dark:[--checkbox-checked-bg:var(--color-white)] dark:[--checkbox-checked-border:var(--color-zinc-950)]/15',
|
||||
],
|
||||
white:
|
||||
'[--checkbox-check:var(--color-zinc-900)] [--checkbox-checked-bg:var(--color-white)] [--checkbox-checked-border:var(--color-zinc-950)]/15',
|
||||
dark: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-900)] [--checkbox-checked-border:var(--color-zinc-950)]/90',
|
||||
zinc: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-zinc-600)] [--checkbox-checked-border:var(--color-zinc-700)]/90',
|
||||
red: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-red-600)] [--checkbox-checked-border:var(--color-red-700)]/90',
|
||||
orange:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-orange-500)] [--checkbox-checked-border:var(--color-orange-600)]/90',
|
||||
amber:
|
||||
'[--checkbox-check:var(--color-amber-950)] [--checkbox-checked-bg:var(--color-amber-400)] [--checkbox-checked-border:var(--color-amber-500)]/80',
|
||||
yellow:
|
||||
'[--checkbox-check:var(--color-yellow-950)] [--checkbox-checked-bg:var(--color-yellow-300)] [--checkbox-checked-border:var(--color-yellow-400)]/80',
|
||||
lime: '[--checkbox-check:var(--color-lime-950)] [--checkbox-checked-bg:var(--color-lime-300)] [--checkbox-checked-border:var(--color-lime-400)]/80',
|
||||
green:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-green-600)] [--checkbox-checked-border:var(--color-green-700)]/90',
|
||||
emerald:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-emerald-600)] [--checkbox-checked-border:var(--color-emerald-700)]/90',
|
||||
teal: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-teal-600)] [--checkbox-checked-border:var(--color-teal-700)]/90',
|
||||
cyan: '[--checkbox-check:var(--color-cyan-950)] [--checkbox-checked-bg:var(--color-cyan-300)] [--checkbox-checked-border:var(--color-cyan-400)]/80',
|
||||
sky: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-sky-500)] [--checkbox-checked-border:var(--color-sky-600)]/80',
|
||||
blue: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-blue-600)] [--checkbox-checked-border:var(--color-blue-700)]/90',
|
||||
indigo:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-indigo-500)] [--checkbox-checked-border:var(--color-indigo-600)]/90',
|
||||
violet:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-violet-500)] [--checkbox-checked-border:var(--color-violet-600)]/90',
|
||||
purple:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-purple-500)] [--checkbox-checked-border:var(--color-purple-600)]/90',
|
||||
fuchsia:
|
||||
'[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-fuchsia-500)] [--checkbox-checked-border:var(--color-fuchsia-600)]/90',
|
||||
pink: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-pink-500)] [--checkbox-checked-border:var(--color-pink-600)]/90',
|
||||
rose: '[--checkbox-check:var(--color-white)] [--checkbox-checked-bg:var(--color-rose-500)] [--checkbox-checked-border:var(--color-rose-600)]/90',
|
||||
}
|
||||
|
||||
type Color = keyof typeof colors
|
||||
|
||||
export function Checkbox({
|
||||
color = 'dark/zinc',
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
color?: Color
|
||||
className?: string
|
||||
} & Omit<Headless.CheckboxProps, 'as' | 'className'>) {
|
||||
return (
|
||||
<Headless.Checkbox
|
||||
data-slot="control"
|
||||
{...props}
|
||||
className={clsx(className, 'group inline-flex focus:outline-hidden')}
|
||||
>
|
||||
<span className={clsx([base, colors[color]])}>
|
||||
<svg
|
||||
className="size-4 stroke-(--checkbox-check) opacity-0 group-data-checked:opacity-100 sm:h-3.5 sm:w-3.5"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
>
|
||||
{/* Checkmark icon */}
|
||||
<path
|
||||
className="opacity-100 group-data-indeterminate:opacity-0"
|
||||
d="M3 8L6 11L11 3.5"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
{/* Indeterminate icon */}
|
||||
<path
|
||||
className="opacity-0 group-data-indeterminate:opacity-100"
|
||||
d="M3 7H11"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Headless.Checkbox>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user