diff --git a/.gitignore b/.gitignore index 4a929c9..4c30b82 100644 --- a/.gitignore +++ b/.gitignore @@ -151,4 +151,15 @@ dist # Vite files vite.config.js.timestamp-* vite.config.ts.timestamp-* -.vite/ \ No newline at end of file +.vite/ + +# JetBrains IDEA +.idea/* + +# OpenCode +.opencode/* +AGENTS.md + +# Playwright MCP +.playwright-mcp/* +/*.png diff --git a/full-stack/src/components/design-system/membership-card.tsx b/full-stack/src/components/design-system/membership-card.tsx index 07ceb70..8eaeea4 100644 --- a/full-stack/src/components/design-system/membership-card.tsx +++ b/full-stack/src/components/design-system/membership-card.tsx @@ -1,5 +1,6 @@ import { cva } from 'class-variance-authority'; import { cn } from '@/lib/utils'; +import { getInitials } from '@/utils/string'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader } from '../ui/card'; import { Detail, Subtitle, Text } from './typography'; @@ -72,13 +73,17 @@ export function MembershipCard({ variant, groupName, roles, + startDate = '2020 mar', endDate, + onOpen, }: { className?: string; variant: 'tier1' | 'tier2' | 'active' | 'alumni' | 'newbie'; groupName: string; roles: string; + startDate?: string; endDate?: string; + onOpen?: () => void; }) { const hasBackground = variant === 'tier1' || variant === 'tier2' || variant === 'active'; @@ -87,6 +92,7 @@ export function MembershipCard({ return ( {/* Group Avatar */}
- S + {getInitials(groupName)}
{/* Group name */} @@ -142,7 +148,7 @@ export function MembershipCard({
- {'2020 mar'} + {startDate} {endDate ? ( <>
diff --git a/full-stack/src/components/design-system/sheet.stories.tsx b/full-stack/src/components/design-system/sheet.stories.tsx deleted file mode 100644 index a529c0b..0000000 --- a/full-stack/src/components/design-system/sheet.stories.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { Button } from '@/components/ui/button'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, - SheetTrigger, -} from './sheet'; - -const meta: Meta = { - title: 'Design System/Sheet', - component: Sheet, - parameters: { - layout: 'centered', - }, - decorators: [ - (Story) => ( -
- -
- ), - ], - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - -export const Right: Story = { - render: () => ( - - - - - - - Edit profile - - Make changes to your profile here. Click save when you're done. - - -
-
- - -
-
- - -
-
- - - -
-
- ), -}; - -export const Left: Story = { - render: () => ( - - - - - - - Navigation - - Access different sections of the app. - - -
- -
-
-
- ), -}; - -export const Top: Story = { - render: () => ( - - - - - - - Notification - - You have a new message from the team. - - -
-

- Hey there! Just wanted to check in and see how things are going. -

-
- - - -
-
- ), -}; - -export const Bottom: Story = { - render: () => ( - - - - - - - Confirm Action - - Are you sure you want to delete this item? This action cannot be - undone. - - - - - - - - - ), -}; - -export const WithoutCloseButton: Story = { - render: () => ( - - - - - - - Custom Close - - This sheet has no built-in close button. Use the footer button - instead. - - -
-

Content goes here...

-
- - - - - -
-
- ), -}; diff --git a/full-stack/src/components/design-system/sheet.tsx b/full-stack/src/components/design-system/sheet.tsx deleted file mode 100644 index f9ccfa8..0000000 --- a/full-stack/src/components/design-system/sheet.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { - SheetContent as ShadSheetContent, - SheetDescription as ShadSheetDescription, - SheetHeader as ShadSheetHeader, - Sheet, - SheetClose, - SheetFooter, - SheetTitle, - SheetTrigger, -} from '@/components/ui/sheet'; -import { cn } from '@/lib/utils'; - -export const SheetContent: typeof ShadSheetContent = ({ - className, - ...params -}) => { - return ( - - ); -}; - -export const SheetHeader: typeof ShadSheetHeader = ({ - className, - ...params -}) => { - return ( - - ); -}; - -export const SheetDescription: typeof ShadSheetDescription = ({ - className, - ...params -}) => { - return ( - - ); -}; - -export { Sheet, SheetClose, SheetFooter, SheetTitle, SheetTrigger }; diff --git a/full-stack/src/components/design-system/user-avatar.stories.tsx b/full-stack/src/components/design-system/user-avatar.stories.tsx index 1f5868a..05acad4 100644 --- a/full-stack/src/components/design-system/user-avatar.stories.tsx +++ b/full-stack/src/components/design-system/user-avatar.stories.tsx @@ -246,21 +246,21 @@ export const AllSizes: Story = { name: 'All Sizes', render: () => (
- + JD - + JD - + {/* Tier 1 row */}
- + T1 - + T1 - + T1
{/* Tier 2 row */}
- + T2 - + T2 - + T2
{/* Tier 3 row */}
- + T3 - + T3 - + T3
{/* Draft row */}
- + DR - + DR - + DR
{/* Inactive row */}
- + IN - + IN - + IN
diff --git a/full-stack/src/components/design-system/user-avatar.tsx b/full-stack/src/components/design-system/user-avatar.tsx index 08e6ffb..9ded8db 100644 --- a/full-stack/src/components/design-system/user-avatar.tsx +++ b/full-stack/src/components/design-system/user-avatar.tsx @@ -8,18 +8,19 @@ export type UserAvatarTier = | 'active' | 'inactive' | 'unknown'; -export type UserAvatarSize = 'sm' | 'default' | 'lg'; +export type AvatarSizeClass = + | 'avatar-small' + | 'avatar-default' + | 'avatar-large'; export interface UserAvatarProps extends ComponentProps { tier?: UserAvatarTier; - size?: UserAvatarSize; showBadge?: boolean; children?: React.ReactNode; } export function UserAvatar({ tier = 'unknown', - size = 'default', showBadge = true, className, children, @@ -29,24 +30,18 @@ export function UserAvatar({ {children} - {showBadge && - size !== 'sm' && - (tier === 'tier-1' || tier === 'tier-2') && ( - - )} + {showBadge && (tier === 'tier-1' || tier === 'tier-2') && ( + + )} ); } @@ -78,8 +73,6 @@ export function UserAvatarFallback({ className={cn( 'flex size-full items-center justify-center rounded-full', 'font-semibold text-lg', - 'data-[size=sm]:text-xs', - 'data-[size=lg]:text-3xl', className )} {...props} @@ -89,13 +82,7 @@ export function UserAvatarFallback({ ); } -function UserAvatarBadge({ - large, - tier, -}: { - large: boolean; - tier: 'tier-1' | 'tier-2'; -}) { +function UserAvatarBadge({ tier }: { tier: 'tier-1' | 'tier-2' }) { const BadgeIcon = tier === 'tier-2' ? KBCard : ABCard; return ( @@ -110,9 +97,7 @@ function UserAvatarBadge({ // Tier gradient background 'bg-linear-to-br', 'data-[tier=tier-1]:from-tier-1-start data-[tier=tier-1]:to-tier-1-end', - 'data-[tier=tier-2]:from-tier-2-start data-[tier=tier-2]:to-tier-2-end', - // Size-based scaling - large && 'origin-bottom-left scale-[2]' + 'data-[tier=tier-2]:from-tier-2-start data-[tier=tier-2]:to-tier-2-end' )} > diff --git a/full-stack/src/components/ui/avatar.tsx b/full-stack/src/components/ui/avatar.tsx index 75c377d..3c1f9e9 100644 --- a/full-stack/src/components/ui/avatar.tsx +++ b/full-stack/src/components/ui/avatar.tsx @@ -5,17 +5,13 @@ import { cn } from "@/lib/utils" function Avatar({ className, - size = "default", ...props -}: React.ComponentProps & { - size?: "default" | "sm" | "lg" -}) { +}: React.ComponentProps) { return ( ) { data-slot="avatar-badge" className={cn( "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", - "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className )} {...props} @@ -91,7 +85,7 @@ function AvatarGroupCount({ return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", className)} + className={cn("bg-muted text-muted-foreground rounded-full text-sm ring-background relative flex shrink-0 items-center justify-center ring-2", className)} {...props} /> ) diff --git a/full-stack/src/components/ui/drawer.tsx b/full-stack/src/components/ui/drawer.tsx deleted file mode 100644 index 8b00c05..0000000 --- a/full-stack/src/components/ui/drawer.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as React from "react" -import { Drawer as DrawerPrimitive } from "vaul" - -import { cn } from "@/lib/utils" - -function Drawer({ - ...props -}: React.ComponentProps) { - return -} - -function DrawerTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function DrawerPortal({ - ...props -}: React.ComponentProps) { - return -} - -function DrawerClose({ - ...props -}: React.ComponentProps) { - return -} - -function DrawerOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DrawerContent({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - -
- {children} - - - ) -} - -function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DrawerTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DrawerDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, -} diff --git a/full-stack/src/components/ui/sheet.stories.tsx b/full-stack/src/components/ui/sheet.stories.tsx new file mode 100644 index 0000000..50bd6a4 --- /dev/null +++ b/full-stack/src/components/ui/sheet.stories.tsx @@ -0,0 +1,114 @@ +import * as React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from '@/components/ui/button'; +import { Sheet, SheetFooter } from './sheet'; + +const meta: Meta = { + title: 'UI/Sheet', + parameters: { layout: 'fullscreen' }, + decorators: [ + (Story) => ( +
+
+ +
+
+ ), + ], + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const [open, setOpen] = React.useState(false); + return ( +
+ + +
+

+ Itt lenne a tag részletes információjának listája, a tagsági + kártyával együtt. +

+
+ + + + +
+
+ ); + }, +}; + +export const NoTitle: Story = { + render: () => { + const [open, setOpen] = React.useState(false); + return ( +
+ + +
+

+ Content without a header area. +

+
+ + + + +
+
+ ); + }, +}; + +export const LongContent: Story = { + render: () => { + const [open, setOpen] = React.useState(false); + return ( +
+ + +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
+ + 2020 ősz #{i} + + + {i * 10} pont + +
+

+ Sint commodi ex impedit architecto unde dolor. Nulla ut quo + quaerat. Quae qui ea dolor natus aut magnam eum. +

+
+ ))} +
+
+
+ ); + }, +}; diff --git a/full-stack/src/components/ui/sheet.tsx b/full-stack/src/components/ui/sheet.tsx index 539cc97..40858e6 100644 --- a/full-stack/src/components/ui/sheet.tsx +++ b/full-stack/src/components/ui/sheet.tsx @@ -1,132 +1,140 @@ -import * as React from "react" -import { Dialog as SheetPrimitive } from "radix-ui" +import * as React from "react"; +import { Dialog as DialogPrimitive } from "radix-ui"; +import { Drawer as DrawerPrimitive } from "vaul"; +import { XIcon } from "lucide-react"; -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { XIcon } from "lucide-react" +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { useIsMobile } from "@/hooks/use-mobile"; -function Sheet({ ...props }: React.ComponentProps) { - return +interface SheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title?: string; + description?: string; + theme?: "dark" | "light"; + showCloseButton?: boolean; + children: React.ReactNode; + className?: string; } -function SheetTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function SheetClose({ - ...props -}: React.ComponentProps) { - return -} - -function SheetPortal({ - ...props -}: React.ComponentProps) { - return -} - -function SheetOverlay({ +function Sheet({ + open, + onOpenChange, + title, + description, + theme, + showCloseButton = true, + children, className, - ...props -}: React.ComponentProps) { - return ( - - ) -} +}: SheetProps) { + const isMobile = useIsMobile(); -function SheetContent({ - className, - children, - side = "right", - showCloseButton = true, - ...props -}: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left" - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - - )} - - - ) -} + if (isMobile) { + return ( + + + + + {showCloseButton && ( + + + + )} + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + {(title || description) && ( +
+ )} + {children} + + + + ); + } -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return ( -
- ) + + + + + {showCloseButton && ( + + + + )} + {(title || description) && ( +
+ {title && ( + + {title} + + )} + {description && ( + + {description} + + )} +
+ )} +
{children}
+
+
+
+ ); } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { return (
- ) -} - -function SheetTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function SheetDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) + ); } -export { - Sheet, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -} +export { Sheet, SheetFooter }; diff --git a/full-stack/src/components/ui/sidebar.tsx b/full-stack/src/components/ui/sidebar.tsx deleted file mode 100644 index 2f536aa..0000000 --- a/full-stack/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,699 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" - -import { useIsMobile } from "@/hooks/use-mobile" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet" -import { Skeleton } from "@/components/ui/skeleton" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" -import { PanelLeftIcon } from "lucide-react" - -const SIDEBAR_COOKIE_NAME = "sidebar_state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 -const SIDEBAR_WIDTH = "16rem" -const SIDEBAR_WIDTH_MOBILE = "18rem" -const SIDEBAR_WIDTH_ICON = "3rem" -const SIDEBAR_KEYBOARD_SHORTCUT = "b" - -type SidebarContextProps = { - state: "expanded" | "collapsed" - open: boolean - setOpen: (open: boolean) => void - openMobile: boolean - setOpenMobile: (open: boolean) => void - isMobile: boolean - toggleSidebar: () => void -} - -const SidebarContext = React.createContext(null) - -function useSidebar() { - const context = React.useContext(SidebarContext) - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider.") - } - - return context -} - -function SidebarProvider({ - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props -}: React.ComponentProps<"div"> & { - defaultOpen?: boolean - open?: boolean - onOpenChange?: (open: boolean) => void -}) { - const isMobile = useIsMobile() - const [openMobile, setOpenMobile] = React.useState(false) - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen) - const open = openProp ?? _open - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value - if (setOpenProp) { - setOpenProp(openState) - } else { - _setOpen(openState) - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` - }, - [setOpenProp, open] - ) - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) - }, [isMobile, setOpen, setOpenMobile]) - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - toggleSidebar() - } - } - - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) - }, [toggleSidebar]) - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed" - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ) - - return ( - -
- {children} -
-
- ) -} - -function Sidebar({ - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - dir, - ...props -}: React.ComponentProps<"div"> & { - side?: "left" | "right" - variant?: "sidebar" | "floating" | "inset" - collapsible?: "offcanvas" | "icon" | "none" -}) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar() - - if (collapsible === "none") { - return ( -
- {children} -
- ) - } - - if (isMobile) { - return ( - - - - Sidebar - Displays the mobile sidebar. - -
{children}
-
-
- ) - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ) -} - -function SidebarTrigger({ - className, - onClick, - ...props -}: React.ComponentProps) { - const { toggleSidebar } = useSidebar() - - return ( - - ) -} - -function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() - - return ( - + ); +} + +function GroupProfilePage() { + const [theme, setTheme] = useState<'dark' | 'light'>('dark'); + const activeCount = MOCK_MEMBERS.filter( + (m) => m.kind !== MembershipKind.FORMER + ).length; + + return ( +
+
+ +
+ +
+
+
+ + {SEMESTER_BADGE} + +

+ {CURRENT_GROUP.name} +

+

+ Alapítva: {FOUNDATION_YEAR} • {OBJECTIVE_TEXT} +

+
+
+
+
+ Aktivitás +
+
+ {TOTAL_ACTIVITY.toLocaleString('en-US')} +
+
+
+
+ Tagok +
+
+ {MOCK_MEMBERS.length} +
+
+
+
+
+ +
+
+
+ {/*TODO org tree component*/} +
+ Struktúra +
    + {[...MOCK_PARENTS, CURRENT_GROUP, ...MOCK_CHILDREN].map( + (group) => ( +
  • + {group.name} +
  • + ) + )} +
+
+ +
+ Cél + + {OBJECTIVE_TEXT} + +
+
+ +
+
+
+ Tagok + + {activeCount} Aktív + +
+ +
+
+ {MOCK_MEMBERS.map((member) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/full-stack/src/routes/user/profile.tsx b/full-stack/src/routes/user/profile.tsx index fe1b792..3395c5d 100644 --- a/full-stack/src/routes/user/profile.tsx +++ b/full-stack/src/routes/user/profile.tsx @@ -1,30 +1,325 @@ -import 'reflect-metadata'; import { createFileRoute } from '@tanstack/react-router'; +import { + Antenna, + AtSign, + ChevronDown, + Clock, + GraduationCap, + Home, + type LucideIcon, + MessageCircle, + Moon, + MoreVertical, + Search, + Send, + Sun, + ThumbsUp, +} from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { MembershipCard } from '@/components/design-system/membership-card'; +import { Text } from '@/components/design-system/typography'; +import { + UserAvatar, + UserAvatarFallback, +} from '@/components/design-system/user-avatar'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { Sheet } from '@/components/ui/sheet'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; +import { getInitials } from '@/utils/string'; export const Route = createFileRoute('/user/profile')({ - component: UserProfileComponent, + component: UserProfilePage, }); -function UserProfileComponent() { - // Client-side component +type GroupVariant = 'tier1' | 'tier2' | 'active' | 'alumni' | 'newbie'; + +interface GroupData { + id: string; + variant: GroupVariant; + groupName: string; + roles: string; + startDate: string; + startUnit?: string; + endDate?: string; +} + +const MOCK_PROFILE = { + name: 'Jane Doe', + nickname: 'Jenny', + handle: '@realijanedo', +}; + +const MOCK_GROUPS: Array = [ + { + id: 'lovagrend-1', + variant: 'tier2', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'körvezető, administrátor', + startDate: '2000 MAR', + }, + { + id: 'lovagrend-2', + variant: 'tier2', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'körvezető, administrátor', + startDate: '2000 MAR', + }, + { + id: 'lovagrend-3', + variant: 'alumni', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'körvezető, administrátor', + startDate: '2000 MAR', + }, + { + id: 'lovagrend-4', + variant: 'alumni', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'körvezető, administrátor', + startDate: '2000 MAR', + }, + { + id: 'lovagrend-5', + variant: 'alumni', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'körvezető, administrátor', + startDate: '2000 MAR', + }, + { + id: 'lovagrend-6', + variant: 'alumni', + groupName: 'Szent Schönherz Senior Lovagrend', + roles: 'alumni', + startDate: '2000 MAR', + endDate: '2025 JUN', + }, +]; + +const INFO_ITEMS: Array<{ icon: LucideIcon; value: string; label: string }> = [ + { icon: Home, value: 'SCH-1313', label: 'Residence' }, + { icon: Clock, value: '2021', label: 'Joined' }, + { icon: GraduationCap, value: '2025', label: 'Last Active' }, +]; + +const MOCK_CONTACTS: Array<{ + id: string; + icon: LucideIcon; + value: string; + label: string; +}> = [ + { id: 'twitter', icon: AtSign, value: 'janedoe', label: 'twitter' }, + { id: 'callsign', icon: Antenna, value: 'HA5KFU', label: 'call sign' }, + { id: 'telegram', icon: Send, value: 'janedoe', label: 'telegram' }, + { id: 'facebook', icon: ThumbsUp, value: 'janedoe', label: 'facebook' }, + { id: 'discord', icon: MessageCircle, value: 'janedoe', label: '(✿‿✿)つ□~' }, +]; + +function ContactButton({ + className, + onClick, +}: { + className?: string; + onClick: () => void; +}) { return ( -
-
-

User Profile

-

- This is an example route demonstrating NestJS-style dependency - injection in TanStack Start. -

-
-

Features:

-
    -
  • Type-safe dependency injection using tsyringe
  • -
  • Request-scoped Prisma service
  • -
  • Authentication and authorization middleware
  • -
  • Automatic resource cleanup after request
  • -
-
+ + ); +} + +function UserProfilePage() { + const [contactOpen, _setContactOpen] = useState(false); + const setContactOpen = useCallback((v: boolean) => { + _setContactOpen(prev => (prev === v ? prev : v)); + }, []); + const [theme, setTheme] = useState<'dark' | 'light'>('dark'); + + function handleOpenContact() { + setContactOpen(true); + } + function handleToggleHandleMenu() {} + function handleSearchSubmit() {} + function handleOpenOverflowMenu() {} + function handleOpenGroup(_group: GroupData) {} + + return ( +
+
+ + +
+ +
+ + Groups + Activity + +
+ + + +
+ + +
+
+
+ + +
+ {MOCK_GROUPS.map((group) => ( + handleOpenGroup(group)} + /> + ))} +
+
+ +
Activity
+
+
+
+ + +
+
+

+ Pék profil +

+

pek.sch.bme.hu

+
+ +
+ +
+
+ {MOCK_CONTACTS.map(({ id, icon: Icon, value, label }) => ( + + ))} +
+
+
); } diff --git a/full-stack/src/styles.css b/full-stack/src/styles.css index e3c4deb..80cbabd 100644 --- a/full-stack/src/styles.css +++ b/full-stack/src/styles.css @@ -34,6 +34,9 @@ --tier-3-end: oklch(0.52 0.1 220); --tier-draft: oklch(0.7 0.06 240 / 60%); --tier-inactive: oklch(0.75 0.01 240); + --status-member: oklch(0.67 0.18 277); + --status-newbie: oklch(0.76 0.18 163); + --status-alumni: var(--muted-foreground); --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); @@ -90,6 +93,9 @@ --tier-3-end: oklch(0.7 0.08 220); --tier-draft: oklch(0.5 0.08 240 / 60%); --tier-inactive: oklch(0.35 0.01 240); + --status-member: oklch(0.67 0.18 277); + --status-newbie: oklch(0.76 0.18 163); + --status-alumni: var(--muted-foreground); --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); @@ -135,6 +141,8 @@ } :root { + --banner: oklch(0.2 0.04 255); + --space-page: 1rem; /* 16px */ --space-section: 1rem; /* 16px */ --space-card: 0.875rem; /* 14px */ @@ -143,7 +151,7 @@ @media (min-width: 768px) { :root { --space-page: 1.5rem; /* 24px */ - --space-section: 1.25rem; /* 20px */ + --space-section: 2rem; /* 32px */ --space-card: 1rem; /* 16px */ --space-inline: 0.75rem; /* 12px */ } @@ -198,8 +206,12 @@ --color-tier-3-end: var(--tier-3-end); --color-tier-draft: var(--tier-draft); --color-tier-inactive: var(--tier-inactive); + --color-status-member: var(--status-member); + --color-status-newbie: var(--status-newbie); + --color-status-alumni: var(--status-alumni); --color-glass: var(--glass); --color-glass-elevated: var(--glass-elevated); + --color-banner: var(--banner); --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); @@ -259,6 +271,49 @@ @apply duration-700; } +@utility avatar-small { + @apply size-10 ring-2; + [data-slot="avatar-fallback"] { + @apply text-xs; + } + [data-slot="user-avatar-badge"], + [data-slot="avatar-badge"] { + display: none; + } + [data-slot="avatar-group-count"] { + @apply size-6 [&>svg]:size-3; + } +} + +@utility avatar-default { + @apply size-30 ring-4; + [data-slot="avatar-fallback"] { + @apply text-lg; + } + [data-slot="avatar-badge"] { + @apply size-2.5 [&>svg]:size-2; + } + [data-slot="avatar-group-count"] { + @apply size-8 [&>svg]:size-4; + } +} + +@utility avatar-large { + @apply size-50 ring-6; + [data-slot="avatar-fallback"] { + @apply text-3xl; + } + [data-slot="user-avatar-badge"] { + @apply origin-bottom-left scale-2; + } + [data-slot="avatar-badge"] { + @apply size-3 [&>svg]:size-2; + } + [data-slot="avatar-group-count"] { + @apply size-10 [&>svg]:size-5; + } +} + @keyframes fadeIn { from { opacity: 0; diff --git a/full-stack/src/utils/string.ts b/full-stack/src/utils/string.ts new file mode 100644 index 0000000..1d67769 --- /dev/null +++ b/full-stack/src/utils/string.ts @@ -0,0 +1,9 @@ +export function getInitials(name: string): string { + const words = name.trim().split(/\s+/); + const firstWord = words[0] ?? ''; + const lastWord = words.at(-1) ?? ''; + if (words.length === 1) { + return firstWord.charAt(0).toUpperCase(); + } + return (firstWord.charAt(0) + lastWord.charAt(0)).toUpperCase(); +}