feat: add Frutiger font, enhance UI with glass effect and shadows, and improve component styling

This commit is contained in:
2025-09-07 01:12:09 +02:00
parent f1e891413a
commit c3539cfc11
16 changed files with 173 additions and 76 deletions

View File

@@ -50,6 +50,7 @@ export function FollowButton({
onClick={handleClick}
disabled={isLoading}
variant={isFollowing ? "secondary" : "default"}
data-following={isFollowing}
>
{isFollowing ? (
<>

View File

@@ -10,7 +10,7 @@ export function Header() {
const { token } = useAuth();
return (
<header className="sticky top-0 z-50 flex justify-center w-full border-b border-primary/20 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60 glossy-effect bottom">
<header className="sticky top-0 z-50 flex justify-center w-full border-b border-primary/20 bg-background/80 glass-effect glossy-effect bottom rounded-none">
<div className="container flex h-14 items-center px-2">
<div className="flex gap-2">
<Link href="/" className="flex items-center gap-1">

View File

@@ -55,7 +55,7 @@ export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 pt-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 p-4">
<FormField
control={form.control}
name="content"
@@ -64,7 +64,7 @@ export function ReplyForm({ parentThoughtId, onReplySuccess }: ReplyFormProps) {
<FormControl>
<Textarea
placeholder="Post your reply..."
className="resize-none"
className="resize-none bg-white glass-effect glossy-efect bottom shadow-fa-sm"
{...field}
/>
</FormControl>

View File

@@ -85,8 +85,10 @@ export function ThoughtCard({
<div
id={thought.id}
className={cn(
"bg-card/70 backdrop-blur-lg shadow-fa-md rounded-xl overflow-hidden glossy-effect bottom",
isReply ? "backdrop-blur-sm shadow-fa-sm p-2" : ""
"bg-transparent backdrop-blur-lg shadow-fa-md rounded-xl overflow-hidden glossy-effect bottom",
isReply
? "bg-white/80 glass-effect glossy-effect bottom shadow-fa-sm p-2"
: ""
)}
>
{thought.replyToId && isReply && (
@@ -163,7 +165,7 @@ export function ThoughtCard({
)}
{isReplyOpen && (
<div className="border-t p-4 border-border/50 bg-background/50 backdrop-blur-sm">
<div className="border-t m-4 rounded-2xl border-border/50 bg-secondary/20 ">
<ReplyForm
parentThoughtId={thought.id}
onReplySuccess={() => setIsReplyOpen(false)}

View File

@@ -25,7 +25,7 @@ export function ThoughtThread({
const directReplies = repliesByParentId.get(thought.id) || [];
return (
<div className="flex flex-col gap-0">
<div id={`thought-thread-${thought.id}`} className="flex flex-col gap-0">
<ThoughtCard
thought={thought}
author={author}
@@ -34,7 +34,10 @@ export function ThoughtThread({
/>
{directReplies.length > 0 && (
<div className="pl-6 border-l-2 border-primary/30 border-dashed ml-6 flex flex-col gap-4 pt-4">
<div
id={`thought-thread-${thought.id}__replies`}
className="pl-6 border-l-2 border-primary border-dashed ml-6 flex flex-col gap-4 pt-4"
>
{directReplies.map((reply) => (
<ThoughtThread // RECURSIVE CALL
key={reply.id}

View File

@@ -40,19 +40,25 @@ export async function TopFriends({ usernames }: TopFriendsProps) {
.map((result) => result.value);
return (
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Top Friends</CardTitle>
<Card id="top-friends" className="p-4">
<CardHeader id="top-friends__header" className="p-0 pb-2">
<CardTitle id="top-friends__title" className="text-lg text-shadow-md">
Top Friends
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<CardContent id="top-friends__content" className="p-0">
{friends.map((friend) => (
<Link
id={`top-friends__link-${friend.id}`}
href={`/users/${friend.username}`}
key={friend.id}
className="flex items-center gap-3 py-2 px-2 -mx-2 rounded-lg hover:bg-accent/50 transition-colors"
>
<UserAvatar src={friend.avatarUrl} alt={friend.username} />
<span className="text-xs truncate w-full group-hover:underline font-medium text-shadow-sm">
<span
id={`top-friends__name-${friend.id}`}
className="text-xs truncate w-full group-hover:underline font-medium text-shadow-sm"
>
{friend.displayName || friend.username}
</span>
</Link>

View File

@@ -11,12 +11,13 @@ const buttonVariants = cva(
variant: {
// Default button gets blue gradient, gloss, and shadows
default:
"fa-gradient-blue text-primary-foreground shadow-fa-md hover:bg-primary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
"glass-effect fa-gradient-blue text-primary-foreground shadow-fa-md hover:bg-primary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
// Secondary gets green gradient
secondary:
"fa-gradient-green text-secondary-foreground shadow-fa-md hover:bg-secondary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
"glass-effect fa-gradient-green text-secondary-foreground shadow-fa-md hover:bg-secondary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
// Ghost and Link should be more subtle
ghost: "hover:bg-accent hover:text-accent-foreground rounded-lg", // Keep them simple, maybe a slight blur/gloss on hover
ghost:
"glass-effect hover:bg-accent hover:text-accent-foreground rounded-lg", // Keep them simple, maybe a slight blur/gloss on hover
link: "text-primary underline-offset-4 hover:underline",
// Outline button for a transparent-ish, glassy feel
outline:

View File

@@ -7,11 +7,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"relative rounded-xl border bg-card/70 backdrop-blur-lg shadow-fa-lg overflow-hidden glossy-effect bottom text-card-foreground flex flex-col gap-6 py-6",
"relative rounded-xl border bg-card/70 backdrop-blur-lg shadow-fa-lg overflow-hidden glass-effect glossy-effect bottom text-card-foreground flex flex-col gap-6 py-6",
className
)}
{...props}
/>
></div>
);
}

View File

@@ -1,27 +1,27 @@
"use client"
"use client";
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
@@ -30,14 +30,14 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"bg-transparent glass-effect glossy-effect bottom shadow-fa-md border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -47,7 +47,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@@ -61,7 +61,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"bg-popover/90 glass-effect shadow-fa-sm bottom text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@@ -82,7 +82,7 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@@ -95,7 +95,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
);
}
function SelectItem({
@@ -107,7 +107,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"bg-transparent focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
@@ -119,7 +119,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@@ -132,7 +132,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@@ -150,7 +150,7 @@ function SelectScrollUpButton({
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
@@ -168,7 +168,7 @@ function SelectScrollDownButton({
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
export {
@@ -182,4 +182,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
}
};

View File

@@ -8,7 +8,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"shadow-fa-inner transition-shadow",
"shadow-fa-inner transition-shadow glass-effect glossy-effect bottom",
className
)}
{...props}