feat(ui): complete phase 4 Poyraz UI foundation

This commit is contained in:
poyrazavsever
2026-07-16 23:20:30 +03:00
parent 5d863280bf
commit 561af11b70
58 changed files with 2555 additions and 7806 deletions
+23 -12
View File
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
import Image from "next/image";
import Link from "next/link";
import { motion, useReducedMotion } from "framer-motion";
import { Typography } from "poyraz-ui/atoms";
import {
ArrowUpRight,
BarChart3,
@@ -13,6 +14,11 @@ import {
} from "lucide-react";
type AuthPageShellProps = {
branding: {
applicationName: string;
lightLogoUrl: string | null;
darkLogoUrl: string | null;
};
title: string;
description: string;
imageSrc?: string;
@@ -31,6 +37,7 @@ const highlights = [
];
export function AuthPageShell({
branding,
title,
description,
form,
@@ -63,8 +70,8 @@ export function AuthPageShell({
<div className="absolute inset-0 opacity-20 bg-[linear-gradient(rgba(255,255,255,.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.18)_1px,transparent_1px)] bg-size-[32px_32px]" />
<div className="relative z-10 flex items-center gap-4 p-10">
<Image
src="/logo/lightLogoLong.png"
alt="Neta"
src={branding.darkLogoUrl ?? "/logo/lightLogoLong.png"}
alt={branding.applicationName}
width={240}
height={64}
className="h-16 w-auto object-contain"
@@ -75,14 +82,18 @@ export function AuthPageShell({
<div className="relative z-10 px-10">
<motion.div {...fadeUp}>
<h1 className="max-w-2xl text-5xl font-semibold leading-[1.02] text-primary-foreground">
<Typography
component="h1"
variant="display"
className="max-w-2xl text-5xl font-semibold leading-[1.02] text-primary-foreground"
>
Freelancer işlerini, müşterilerini ve finansını tek yerde yönet.
</h1>
<p className="mt-6 max-w-xl text-lg leading-8 text-primary-foreground/78">
Neta, günlük operasyonunu, projelerini, side projectlerini ve
</Typography>
<Typography component="p" variant="lead" className="mt-6 max-w-xl text-lg leading-8 text-primary-foreground/78">
{branding.applicationName}, günlük operasyonunu, projelerini, side projectlerini ve
temel finans durumunu sade raporlarla takip etmen için
tasarlanır.
</p>
</Typography>
</motion.div>
<motion.div
@@ -136,8 +147,8 @@ export function AuthPageShell({
>
<div className="mb-8 flex justify-center lg:hidden">
<Image
src="/logo/blackLogoLong.png"
alt="Neta logo"
src={branding.lightLogoUrl ?? "/logo/blackLogoLong.png"}
alt={`${branding.applicationName} logo`}
width={180}
height={56}
className="h-14 w-auto object-contain"
@@ -147,10 +158,10 @@ export function AuthPageShell({
</div>
<div className="space-y-2 text-center lg:text-left">
<h2 className="text-3xl font-semibold tracking-normal text-foreground">
<Typography component="h2" variant="h1" className="text-3xl font-semibold tracking-normal text-foreground">
{title}
</h2>
<p className="text-sm leading-6 text-muted-foreground">{description}</p>
</Typography>
<Typography component="p" variant="muted" className="text-sm leading-6">{description}</Typography>
</div>
<div className="mt-8 space-y-6">
+2 -4
View File
@@ -1,8 +1,7 @@
"use client";
import { useFormStatus } from "react-dom";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { Button } from "poyraz-ui/atoms";
import React from "react";
interface SubmitButtonProps extends React.ComponentProps<typeof Button> {
@@ -18,10 +17,9 @@ export function SubmitButton({
const { pending } = useFormStatus();
return (
<Button type={type} disabled={pending} {...props}>
<Button type={type} disabled={pending} loading={pending} aria-busy={pending} {...props}>
{pending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{pendingText || children}
</>
) : (
+5 -5
View File
@@ -1,14 +1,14 @@
'use client'
"use client";
import { useEffect } from 'react'
import { showToast } from '@/components/ui/toast'
import { useEffect } from "react";
import { toast } from "poyraz-ui/molecules";
export function ErrorToaster({ message }: { message: string }) {
useEffect(() => {
if (message) {
showToast({ message, tone: 'error' })
toast.error(message);
}
}, [message])
return null
return null;
}
+270 -260
View File
@@ -1,16 +1,43 @@
"use client";
import { signOut } from "@/app/login/actions";
import { IconButton } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { PendingLink } from "@/components/ui/pending-link";
import { cn } from "@/lib/utils";
import { ChevronUp, LogOut, Menu, Settings } from "lucide-react";
import {
Card,
CardContent,
Typography,
} from "poyraz-ui/atoms";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "poyraz-ui/molecules";
import {
SidebarBranding,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuItem,
SidebarPanel,
SidebarProvider,
SidebarRail,
SidebarTrigger,
SidebarUserProfile,
useSidebar,
} from "poyraz-ui/organisms";
import { ChevronUp, LogOut, Settings } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";
export type AppShellNavItem = {
title: string;
@@ -23,6 +50,13 @@ export type AppShellNavGroup = {
items: AppShellNavItem[];
};
export type AppShellBranding = {
applicationName: string;
organizationName: string | null;
lightLogoUrl: string | null;
darkLogoUrl: string | null;
};
type ShellUser = {
email: string;
displayName: string;
@@ -31,6 +65,7 @@ type ShellUser = {
};
type AppShellProps = {
branding: AppShellBranding;
children: React.ReactNode;
homeHref: string;
navGroups: AppShellNavGroup[];
@@ -40,6 +75,7 @@ type AppShellProps = {
};
export function AppShell({
branding,
children,
homeHref,
navGroups,
@@ -48,173 +84,206 @@ export function AppShell({
progress,
}: AppShellProps) {
const pathname = usePathname();
const [mobileSidebarState, setMobileSidebarState] = useState({
open: false,
pathname,
});
const isMobileSidebarOpen =
mobileSidebarState.open && mobileSidebarState.pathname === pathname;
const sidebarProps = { branding, homeHref, navGroups, pathname, progress, settingsHref, user };
return (
<div className="min-h-screen bg-background text-foreground">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-ring"
>
Ana içeriğe geç
</a>
<div className="flex min-h-screen">
<AppSidebar
homeHref={homeHref}
navGroups={navGroups}
pathname={pathname}
progress={progress}
settingsHref={settingsHref}
user={user}
className="sticky top-0 hidden h-screen shrink-0 self-stretch lg:flex"
/>
<TooltipProvider>
<div className="min-h-screen bg-background text-foreground">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-focus-ring"
>
Ana içeriğe geç
</a>
{isMobileSidebarOpen ? (
<button
type="button"
aria-label="Menüyü kapat"
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
onClick={() => setMobileSidebarState({ open: false, pathname })}
/>
) : null}
<div className="flex min-h-screen">
<DesktopSidebar {...sidebarProps} />
<AppSidebar
homeHref={homeHref}
navGroups={navGroups}
pathname={pathname}
progress={progress}
settingsHref={settingsHref}
user={user}
className={cn(
"fixed inset-y-0 left-0 z-50 transition-transform duration-200 ease-out lg:hidden",
isMobileSidebarOpen ? "translate-x-0" : "-translate-x-full",
)}
onNavigate={() => setMobileSidebarState({ open: false, pathname })}
/>
<div className="flex min-w-0 flex-1 flex-col">
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-4 backdrop-blur lg:hidden">
<Link href={homeHref} className="flex items-center gap-2 font-semibold">
<Image
src="/logo/blackLogoLong.png"
alt="Neta"
width={120}
height={32}
className="h-8 w-auto object-contain"
style={{ width: "auto" }}
priority
/>
</Link>
<IconButton
label="Menüyü aç"
variant="outline"
onClick={() => setMobileSidebarState({ open: true, pathname })}
>
<Menu className="h-4 w-4" />
</IconButton>
</header>
<main id="main-content" className="min-w-0 flex-1 p-4 lg:p-8">
{children}
</main>
<div className="flex min-w-0 flex-1 flex-col">
<MobileSidebar {...sidebarProps} />
<main id="main-content" className="min-w-0 flex-1 p-4 lg:p-8">
{children}
</main>
</div>
</div>
</div>
</div>
</TooltipProvider>
);
}
function AppSidebar({
homeHref,
navGroups,
pathname,
progress,
settingsHref,
user,
onNavigate,
className,
}: {
type SidebarCompositionProps = {
branding: AppShellBranding;
homeHref: string;
navGroups: AppShellNavGroup[];
pathname: string;
progress?: number;
settingsHref: string;
user: ShellUser;
onNavigate?: () => void;
className?: string;
}) {
};
function DesktopSidebar(props: SidebarCompositionProps) {
return (
<aside
className={cn(
"flex h-dvh w-[280px] max-w-[82vw] flex-col overflow-hidden border-r border-border bg-surface",
className,
)}
>
<div className="shrink-0 px-6 py-3">
<Link href={homeHref} className="flex w-full items-center justify-center">
<Image
src="/logo/blackLogoLong.png"
alt="Neta"
width={160}
height={48}
className="h-12 w-auto object-contain"
style={{ width: "auto" }}
priority
<SidebarProvider variant="collapsible">
<SidebarPanel className="sticky top-0 hidden h-screen shrink-0 self-stretch lg:flex">
<SidebarComposition {...props} />
<SidebarRail aria-label="Kenar çubuğunu daralt veya genişlet" />
</SidebarPanel>
</SidebarProvider>
);
}
function MobileSidebar(props: SidebarCompositionProps) {
return (
<SidebarProvider variant="floating">
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-4 backdrop-blur lg:hidden">
<Link href={props.homeHref} className="min-w-0">
<MobileBrand branding={props.branding} />
</Link>
<Tooltip>
<TooltipTrigger asChild>
<SidebarTrigger action="mobile" aria-label="Ana menüyü aç veya kapat" />
</TooltipTrigger>
<TooltipContent>Menü</TooltipContent>
</Tooltip>
</header>
<SidebarPanel className="h-dvh max-w-[82vw] lg:hidden">
<SidebarComposition {...props} />
</SidebarPanel>
</SidebarProvider>
);
}
function SidebarComposition({
branding,
homeHref,
navGroups,
pathname,
progress,
settingsHref,
user,
}: SidebarCompositionProps) {
return (
<>
<SidebarHeader>
<Link href={homeHref} className="min-w-0 flex-1" aria-label={`${branding.applicationName} ana sayfa`}>
<SidebarBranding
logo={<BrandMark branding={branding} />}
title={branding.applicationName}
subtitle={branding.organizationName ?? "Freelancer portalı"}
/>
</Link>
</div>
<Tooltip>
<TooltipTrigger asChild>
<SidebarTrigger
className="hidden lg:inline-flex"
aria-label="Kenar çubuğunu daralt veya genişlet"
/>
</TooltipTrigger>
<TooltipContent>Kenar çubuğunu daralt</TooltipContent>
</Tooltip>
</SidebarHeader>
<div className="h-px bg-border" />
<SidebarContent scrollMode="fade">
<SidebarNavigation
homeHref={homeHref}
navGroups={navGroups}
pathname={pathname}
/>
</SidebarContent>
<nav className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto px-4 py-5">
{navGroups.map((group, groupIndex) => (
<section key={group.title} className={cn(groupIndex > 0 && "mt-6")}>
<h2 className="px-2 text-[11px] font-semibold uppercase leading-6 text-muted-foreground">
{group.title}
</h2>
<ul className="mt-2 space-y-1">
{group.items.map((item) => {
const isActive =
item.href === homeHref
? pathname === homeHref
: item.href
? pathname === item.href || pathname.startsWith(`${item.href}/`)
: false;
const Icon = item.icon;
return (
<li key={item.href || item.title}>
<PendingLink
href={item.href || "#"}
onClick={onNavigate}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex h-10 items-center gap-3 rounded-md px-3 text-sm font-medium text-muted-foreground transition-colors",
"hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isActive && "bg-primary/10 text-primary",
)}
showSpinner
>
{Icon ? <Icon className="h-4 w-4 shrink-0" /> : null}
<span className="truncate">{item.title}</span>
</PendingLink>
</li>
);
})}
</ul>
</section>
))}
</nav>
<div className="mt-auto flex shrink-0 flex-col gap-4 border-t border-border px-4 py-4">
<SidebarFooter className="flex flex-col gap-3">
{typeof progress === "number" ? <ProgressSummary progress={progress} /> : null}
<AccountMenu user={user} settingsHref={settingsHref} onNavigate={onNavigate} />
</div>
</aside>
<AccountMenu user={user} settingsHref={settingsHref} />
</SidebarFooter>
</>
);
}
function SidebarNavigation({
homeHref,
navGroups,
pathname,
}: Pick<SidebarCompositionProps, "homeHref" | "navGroups" | "pathname">) {
const { setMobileOpen, variant } = useSidebar();
return navGroups.map((group) => (
<SidebarGroup key={group.title}>
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
<SidebarMenu>
{group.items.map((item) => {
const active =
item.href === homeHref
? pathname === homeHref
: item.href
? pathname === item.href || pathname.startsWith(`${item.href}/`)
: false;
const Icon = item.icon;
return (
<SidebarMenuItem
key={item.href || item.title}
href={item.href || "#"}
active={active}
icon={Icon ? <Icon className="h-4 w-4" aria-hidden="true" /> : undefined}
onClick={() => {
if (variant === "floating") setMobileOpen(false);
}}
>
{item.title}
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
));
}
function BrandMark({ branding }: { branding: AppShellBranding }) {
const logoUrl = branding.lightLogoUrl ?? branding.darkLogoUrl;
if (!logoUrl) {
return (
<span className="flex h-full w-full items-center justify-center text-sm font-bold text-primary">
{branding.applicationName.slice(0, 1).toLocaleUpperCase("tr-TR")}
</span>
);
}
return (
<span className="flex h-full w-full items-center justify-center">
<Image
src={logoUrl}
alt=""
width={32}
height={32}
unoptimized
className={branding.darkLogoUrl ? "h-full w-full object-contain dark:hidden" : "h-full w-full object-contain"}
/>
{branding.darkLogoUrl ? (
<Image
src={branding.darkLogoUrl}
alt=""
width={32}
height={32}
unoptimized
className="hidden h-full w-full object-contain dark:block"
/>
) : null}
</span>
);
}
function MobileBrand({ branding }: { branding: AppShellBranding }) {
return (
<div className="flex min-w-0 items-center gap-2">
<span className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-sm border border-border bg-muted">
<BrandMark branding={branding} />
</span>
<Typography component="span" variant="small" className="truncate font-semibold">
{branding.applicationName}
</Typography>
</div>
);
}
@@ -222,16 +291,25 @@ function ProgressSummary({ progress }: { progress: number }) {
const normalizedProgress = Math.max(0, Math.min(100, progress));
return (
<Card className="overflow-hidden border-primary/10 bg-primary/5 shadow-none">
<CardContent className="space-y-3 p-4">
<div className="text-sm font-semibold text-foreground">Proje ilerlemesi</div>
<Card variant="soft" className="w-full overflow-hidden border-primary/10 shadow-none">
<CardContent className="space-y-3 p-3">
<Typography variant="small" className="font-semibold">
Proje ilerlemesi
</Typography>
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs font-medium">
<span className="text-primary">%{normalizedProgress} tamamlandı</span>
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-primary/10">
<Typography variant="caption" className="font-medium text-primary">
%{normalizedProgress} tamamlandı
</Typography>
<div
role="progressbar"
aria-label="Proje ilerlemesi"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={normalizedProgress}
className="h-2 w-full overflow-hidden rounded-full bg-primary-muted"
>
<div
className="h-full rounded-full bg-primary transition-[width] duration-700 ease-out"
className="h-full rounded-full bg-primary transition-[width] duration-700 ease-out motion-reduce:transition-none"
style={{ width: `${normalizedProgress}%` }}
/>
</div>
@@ -241,110 +319,42 @@ function ProgressSummary({ progress }: { progress: number }) {
);
}
function AccountMenu({
user,
settingsHref,
onNavigate,
}: {
user: ShellUser;
settingsHref: string;
onNavigate?: () => void;
}) {
const [open, setOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
function handlePointerDown(event: PointerEvent) {
if (!menuRef.current?.contains(event.target as Node)) {
setOpen(false);
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
setOpen(false);
buttonRef.current?.focus();
}
}
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: string }) {
return (
<div ref={menuRef} className="relative">
<button
ref={buttonRef}
type="button"
aria-expanded={open}
aria-haspopup="menu"
className="flex w-full items-center gap-3 rounded-md p-2 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => setOpen((current) => !current)}
>
<UserAvatar user={user} />
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-foreground">{user.displayName}</div>
<div className="truncate text-xs text-muted-foreground">{user.email}</div>
</div>
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
{open ? (
<div
role="menu"
className="absolute bottom-[calc(100%+0.5rem)] left-0 z-[70] w-full rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="Hesap menüsünü aç"
className="flex w-full items-center gap-2 rounded-sm p-1 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-focus-ring"
>
<PendingLink
href={settingsHref}
role="menuitem"
onClick={() => {
setOpen(false);
onNavigate?.();
}}
className="flex h-9 items-center gap-2 rounded-sm px-3 text-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
showSpinner
<SidebarUserProfile
className="min-w-0 flex-1"
name={user.displayName}
role={user.email}
avatarUrl={user.avatarUrl ?? undefined}
initials={user.shortName}
/>
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="top" className="min-w-52">
<DropdownMenuItem asChild media={<Settings className="h-4 w-4" aria-hidden="true" />}>
<Link href={settingsHref}>Ayarlar</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<form action={signOut}>
<DropdownMenuItem
asChild
media={<LogOut className="h-4 w-4" aria-hidden="true" />}
className="text-destructive"
>
<Settings className="h-4 w-4" />
Ayarlar
</PendingLink>
<div className="my-1 h-px bg-border" />
<form action={signOut}>
<button
type="submit"
role="menuitem"
className="flex h-9 w-full items-center gap-2 rounded-sm px-3 text-left text-sm text-destructive hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<LogOut className="h-4 w-4" />
<button type="submit" className="w-full">
Çıkış yap
</button>
</form>
</div>
) : null}
</div>
);
}
function UserAvatar({ user }: { user: ShellUser }) {
if (user.avatarUrl) {
return (
<Image
src={user.avatarUrl}
alt=""
width={36}
height={36}
className="h-9 w-9 rounded-full object-cover"
/>
);
}
return (
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
{user.shortName}
</span>
</DropdownMenuItem>
</form>
</DropdownMenuContent>
</DropdownMenu>
);
}
+4 -3
View File
@@ -1,9 +1,10 @@
"use client";
import { AppShell } from "@/components/layout/app-shell";
import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
import { sidebarData } from "@/config/sidebar";
type DashboardShellProps = {
branding: AppShellBranding;
children: React.ReactNode;
user: {
email: string;
@@ -13,9 +14,9 @@ type DashboardShellProps = {
};
};
export function DashboardShell({ children, user }: DashboardShellProps) {
export function DashboardShell({ branding, children, user }: DashboardShellProps) {
return (
<AppShell homeHref="/" navGroups={sidebarData} settingsHref="/settings" user={user}>
<AppShell branding={branding} homeHref="/" navGroups={sidebarData} settingsHref="/settings" user={user}>
{children}
</AppShell>
);
+4 -2
View File
@@ -1,9 +1,10 @@
"use client";
import { AppShell } from "@/components/layout/app-shell";
import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
import { portalSidebarData } from "@/config/portal-sidebar";
type PortalShellProps = {
branding: AppShellBranding;
children: React.ReactNode;
user: {
email: string;
@@ -14,9 +15,10 @@ type PortalShellProps = {
progress?: number;
};
export function PortalShell({ children, user, progress }: PortalShellProps) {
export function PortalShell({ branding, children, user, progress }: PortalShellProps) {
return (
<AppShell
branding={branding}
homeHref="/portal"
navGroups={portalSidebarData}
settingsHref="/portal/settings"
@@ -0,0 +1,61 @@
"use client";
import { Button } from "poyraz-ui/atoms";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "poyraz-ui/molecules";
type DestructiveConfirmationProps = {
cancelLabel?: string;
confirmLabel: string;
description: string;
loading?: boolean;
onConfirm: () => void;
onOpenChange: (open: boolean) => void;
open: boolean;
title: string;
};
export function DestructiveConfirmation({
cancelLabel = "Vazgeç",
confirmLabel,
description,
loading = false,
onConfirm,
onOpenChange,
open,
title,
}: DestructiveConfirmationProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent surface="solid" radius="lg" mobile="floating">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline" disabled={loading}>
{cancelLabel}
</Button>
</DialogClose>
<Button
type="button"
variant="destructive"
loading={loading}
aria-busy={loading}
onClick={onConfirm}
>
{confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+69
View File
@@ -0,0 +1,69 @@
import { Button, Card, CardContent, Skeleton, Typography } from "poyraz-ui/atoms";
import { Alert, AlertDescription, AlertTitle } from "poyraz-ui/molecules";
import { Ban, CircleAlert, Inbox } from "lucide-react";
import type { ReactNode } from "react";
type FeedbackStateProps = {
action?: ReactNode;
description: string;
title: string;
variant: "empty" | "error" | "forbidden";
};
export function FeedbackState({ action, description, title, variant }: FeedbackStateProps) {
if (variant === "empty") {
return (
<Card variant="soft">
<CardContent className="flex min-h-56 flex-col items-center justify-center gap-3 p-8 text-center">
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-primary-muted text-primary-muted-foreground">
<Inbox className="h-5 w-5" aria-hidden="true" />
</span>
<div className="space-y-1">
<Typography component="h2" variant="h4">{title}</Typography>
<Typography component="p" variant="muted" className="max-w-lg">{description}</Typography>
</div>
{action}
</CardContent>
</Card>
);
}
const forbidden = variant === "forbidden";
return (
<Alert
role="alert"
variant={forbidden ? "warning" : "destructive"}
appearance="soft"
icon={forbidden ? <Ban aria-hidden="true" /> : <CircleAlert aria-hidden="true" />}
>
<AlertTitle>{title}</AlertTitle>
<AlertDescription className="space-y-3">
<p>{description}</p>
{action}
</AlertDescription>
</Alert>
);
}
export function LoadingState({ label = "İçerik yükleniyor" }: { label?: string }) {
return (
<div className="space-y-4" aria-busy="true" aria-label={label} role="status">
<span className="sr-only">{label}</span>
<Skeleton className="h-8 w-2/5" />
<Skeleton className="h-4 w-3/5" />
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-36 w-full" />
))}
</div>
</div>
);
}
export function RetryAction({ onClick }: { onClick: () => void }) {
return (
<Button type="button" variant="outline" size="sm" onClick={onClick}>
Yeniden dene
</Button>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { Typography } from "poyraz-ui/atoms";
import type { ReactNode } from "react";
type PageHeaderProps = {
title: string;
description?: string;
eyebrow?: string;
primaryAction?: ReactNode;
secondaryActions?: ReactNode;
};
export function PageHeader({
title,
description,
eyebrow,
primaryAction,
secondaryActions,
}: PageHeaderProps) {
return (
<header className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
<div className="min-w-0 space-y-1.5">
{eyebrow ? (
<Typography component="p" variant="caption" className="font-semibold uppercase tracking-wider text-primary">
{eyebrow}
</Typography>
) : null}
<Typography component="h1" variant="h1" balance>
{title}
</Typography>
{description ? (
<Typography component="p" variant="muted" className="max-w-3xl">
{description}
</Typography>
) : null}
</div>
{primaryAction || secondaryActions ? (
<div className="flex shrink-0 flex-wrap items-center gap-2">
{secondaryActions}
{primaryAction}
</div>
) : null}
</header>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { Badge } from "poyraz-ui/atoms";
import type { ComponentProps } from "react";
const statusPresentation = {
accepted: { label: "Kabul edildi", variant: "success" },
active: { label: "Aktif", variant: "success" },
archived: { label: "Arşivlendi", variant: "outline" },
cancelled: { label: "İptal edildi", variant: "outline" },
completed: { label: "Tamamlandı", variant: "success" },
done: { label: "Tamamlandı", variant: "success" },
draft: { label: "Taslak", variant: "secondary" },
expired: { label: "Süresi doldu", variant: "destructive" },
in_progress: { label: "Devam ediyor", variant: "info" },
overdue: { label: "Gecikmiş", variant: "destructive" },
paid: { label: "Ödendi", variant: "success" },
paused: { label: "Duraklatıldı", variant: "warning" },
pending: { label: "Bekliyor", variant: "warning" },
planned: { label: "Planlandı", variant: "secondary" },
planning: { label: "Planlanıyor", variant: "secondary" },
rejected: { label: "Reddedildi", variant: "destructive" },
revoked: { label: "İptal edildi", variant: "destructive" },
sent: { label: "Gönderildi", variant: "info" },
todo: { label: "Yapılacak", variant: "secondary" },
} as const satisfies Record<string, { label: string; variant: NonNullable<ComponentProps<typeof Badge>["variant"]> }>;
export type NetaStatus = keyof typeof statusPresentation;
type StatusBadgeProps = Omit<ComponentProps<typeof Badge>, "children" | "variant"> & {
status: NetaStatus;
};
export function StatusBadge({ status, ...props }: StatusBadgeProps) {
const presentation = statusPresentation[status];
return (
<Badge variant={presentation.variant} {...props}>
{presentation.label}
</Badge>
);
}
export { statusPresentation };
-11
View File
@@ -1,11 +0,0 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
-77
View File
@@ -1,77 +0,0 @@
"use client";
import { forwardRef } from "react";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/utils";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg" | "icon" | "icon-sm";
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
const variantClasses: Record<ButtonVariant, string> = {
primary:
"border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover active:bg-primary-pressed",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-accent active:bg-accent-hover",
outline:
"border-border-strong bg-surface text-foreground shadow-sm hover:bg-accent active:bg-accent-hover",
ghost:
"border-transparent bg-transparent text-foreground hover:bg-accent active:bg-accent-hover",
danger:
"border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover active:bg-destructive-pressed",
};
const sizeClasses: Record<ButtonSize, string> = {
sm: "h-8 gap-1.5 px-3 text-xs",
md: "h-10 gap-2 px-4 text-sm",
lg: "h-11 gap-2.5 px-5 text-sm",
icon: "h-9 w-9 p-0",
"icon-sm": "h-8 w-8 p-0",
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "primary", size = "md", type = "button", ...props }, ref) => (
<button
ref={ref}
type={type}
className={cn(
"inline-flex shrink-0 items-center justify-center rounded-md border font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
variantClasses[variant],
sizeClasses[size],
className,
)}
{...props}
/>
),
);
Button.displayName = "Button";
export type IconButtonProps = Omit<ButtonProps, "children" | "size"> & {
label: string;
tooltip?: string;
children: ReactNode;
};
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
({ label, tooltip, children, className, ...props }, ref) => (
<Button
ref={ref}
size="icon"
aria-label={label}
title={tooltip ?? label}
className={className}
{...props}
>
{children}
</Button>
),
);
IconButton.displayName = "IconButton";
-28
View File
@@ -1,28 +0,0 @@
"use client";
import type { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";
export function Card({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return (
<div
className={cn(
"rounded-md border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
);
}
export function CardHeader({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return <div className={cn("space-y-1.5 p-5", className)} {...props} />;
}
export function CardTitle({ className, ...props }: ComponentPropsWithoutRef<"h3">) {
return <h3 className={cn("text-base font-semibold", className)} {...props} />;
}
export function CardContent({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return <div className={cn("p-5 pt-0", className)} {...props} />;
}
-33
View File
@@ -1,33 +0,0 @@
"use client"
import * as React from "react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
-168
View File
@@ -1,168 +0,0 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" asChild>
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
>
<XIcon
/>
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
-269
View File
@@ -1,269 +0,0 @@
"use client"
import * as React from "react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CheckIcon, ChevronRightIcon } from "lucide-react"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
align = "start",
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
align={align}
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 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 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-checkbox-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span
className="pointer-events-none absolute right-2 flex items-center justify-center"
data-slot="dropdown-menu-radio-item-indicator"
>
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon
/>
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
-51
View File
@@ -1,51 +0,0 @@
"use client";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { cn } from "@/lib/utils";
export function Label({ className, ...props }: ComponentPropsWithoutRef<"label">) {
return (
<label
className={cn("text-sm font-medium leading-none text-foreground", className)}
{...props}
/>
);
}
type FieldProps = ComponentPropsWithoutRef<"div"> & {
label?: ReactNode;
description?: ReactNode;
error?: ReactNode;
htmlFor?: string;
};
export function Field({
label,
description,
error,
htmlFor,
children,
className,
...props
}: FieldProps) {
const descriptionId = htmlFor && description ? `${htmlFor}-description` : undefined;
const errorId = htmlFor && error ? `${htmlFor}-error` : undefined;
return (
<div className={cn("space-y-2", className)} {...props}>
{label ? <Label htmlFor={htmlFor}>{label}</Label> : null}
{children}
{description ? (
<p id={descriptionId} className="text-xs leading-5 text-muted-foreground">
{description}
</p>
) : null}
{error ? (
<p id={errorId} role="alert" className="text-xs leading-5 text-destructive">
{error}
</p>
) : null}
</div>
);
}
-178
View File
@@ -1,178 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
if (!itemContext) {
throw new Error("useFormField should be used within <FormItem>")
}
const fieldState = getFieldState(fieldContext.name, formState)
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
-7
View File
@@ -1,7 +0,0 @@
'use client';
import { Icon as IconifyIcon } from '@iconify/react';
export function Icon({ icon, className }: { icon: string; className?: string }) {
return <IconifyIcon icon={icon} className={className} />;
}
-30
View File
@@ -1,30 +0,0 @@
"use client";
import { forwardRef } from "react";
import type { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
import { cn } from "@/lib/utils";
const controlClasses =
"w-full rounded-md border border-input bg-input-bg px-3 text-sm text-foreground shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-destructive";
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => (
<input ref={ref} className={cn(controlClasses, "h-10", className)} {...props} />
));
Input.displayName = "Input";
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => (
<textarea
ref={ref}
className={cn(controlClasses, "min-h-24 py-2", className)}
{...props}
/>
),
);
Textarea.displayName = "Textarea";
-24
View File
@@ -1,24 +0,0 @@
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+13 -18
View File
@@ -1,26 +1,21 @@
"use client";
import { useEffect, useState } from "react";
import { useSyncExternalStore } from "react";
import { WifiOff } from "lucide-react";
export function OfflineIndicator() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// Sadece client-side'da çalışır
setIsOnline(navigator.onLine);
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
const isOnline = useSyncExternalStore(
(onStoreChange) => {
window.addEventListener("online", onStoreChange);
window.addEventListener("offline", onStoreChange);
return () => {
window.removeEventListener("online", onStoreChange);
window.removeEventListener("offline", onStoreChange);
};
},
() => navigator.onLine,
() => true,
);
if (isOnline) return null;
+3 -7
View File
@@ -4,7 +4,6 @@ import { Loader2 } from "lucide-react";
import Link, { type LinkProps } from "next/link";
import { usePathname } from "next/navigation";
import {
useEffect,
useState,
type AnchorHTMLAttributes,
type MouseEvent,
@@ -33,11 +32,8 @@ export function PendingLink({
...props
}: PendingLinkProps) {
const pathname = usePathname();
const [pending, setPending] = useState(false);
useEffect(() => {
setPending(false);
}, [pathname]);
const [pendingFromPath, setPendingFromPath] = useState<string | null>(null);
const pending = pendingFromPath === pathname;
function handleClick(event: MouseEvent<HTMLAnchorElement>) {
onClick?.(event);
@@ -58,7 +54,7 @@ export function PendingLink({
const nextPath = hrefValue.split("?")[0].split("#")[0];
if (nextPath && nextPath !== pathname) {
setPending(true);
setPendingFromPath(pathname);
}
}
+2 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Button } from "poyraz-ui/atoms";
import type { ComponentProps, ReactNode } from "react";
import { useFormStatus } from "react-dom";
@@ -34,6 +34,7 @@ export function PendingSubmitButton({
type={type}
disabled={disabled || pending}
aria-busy={pending}
loading={pending && !pendingIcon}
className={cn(className)}
>
{icon}
-192
View File
@@ -1,192 +0,0 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", 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 )}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground 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}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
-28
View File
@@ -1,28 +0,0 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }
-12
View File
@@ -1,12 +0,0 @@
import type { ComponentPropsWithoutRef } from "react";
import { cn } from "@/lib/utils";
export function Skeleton({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return (
<div
aria-hidden="true"
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
-18
View File
@@ -1,18 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }
-157
View File
@@ -1,157 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import type { ComponentPropsWithoutRef, ReactElement } from "react";
import { X } from "lucide-react";
import { IconButton } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type ToastTone = "success" | "error" | "info";
type ToastItem = {
id: number;
message: string;
tone: ToastTone;
};
type ToastPayload = {
message: string;
tone?: ToastTone;
};
const toastEventName = "neta:toast";
let toastId = 0;
export function showToast(payload: ToastPayload) {
if (typeof window === "undefined") {
return;
}
window.dispatchEvent(new CustomEvent<ToastPayload>(toastEventName, { detail: payload }));
}
export function Toaster() {
const [items, setItems] = useState<ToastItem[]>([]);
useEffect(() => {
function handleToast(event: Event) {
const customEvent = event as CustomEvent<ToastPayload>;
const item = {
id: ++toastId,
message: customEvent.detail.message,
tone: customEvent.detail.tone ?? "info",
};
setItems((current) => [...current.slice(-2), item]);
window.setTimeout(() => {
setItems((current) => current.filter((toast) => toast.id !== item.id));
}, 4500);
}
window.addEventListener(toastEventName, handleToast);
return () => window.removeEventListener(toastEventName, handleToast);
}, []);
return (
<div
aria-live="polite"
aria-relevant="additions text"
className="fixed bottom-4 right-4 z-[80] flex w-[min(calc(100vw-2rem),24rem)] flex-col gap-2"
>
{items.map((item) => (
<div
key={item.id}
className={cn(
"flex items-start gap-3 rounded-md border bg-surface p-4 text-sm shadow-lg",
item.tone === "error" && "border-destructive/30",
item.tone === "success" && "border-success/30",
)}
>
<div
className={cn(
"mt-1 h-2 w-2 shrink-0 rounded-full bg-info",
item.tone === "error" && "bg-destructive",
item.tone === "success" && "bg-success",
)}
/>
<p className="min-w-0 flex-1 text-foreground">{item.message}</p>
<IconButton
label="Bildirimi kapat"
variant="ghost"
className="-mr-2 -mt-2 h-8 w-8"
onClick={() => setItems((current) => current.filter((toast) => toast.id !== item.id))}
>
<X className="h-4 w-4" />
</IconButton>
</div>
))}
</div>
);
}
export type ToastProps = ComponentPropsWithoutRef<"div"> & {
open?: boolean;
onOpenChange?: (open: boolean) => void;
variant?: "default" | "destructive";
};
export type ToastActionElement = ReactElement;
export function ToastProvider({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Toast({ open = true, onOpenChange, variant, className, ...props }: ToastProps) {
if (!open) {
return null;
}
return (
<div
role={variant === "destructive" ? "alert" : "status"}
className={cn(
"group pointer-events-auto flex w-full items-start gap-3 rounded-md border bg-surface p-4 text-sm shadow-lg",
variant === "destructive" && "border-destructive/30",
className,
)}
data-on-open-change={onOpenChange ? "" : undefined}
{...props}
/>
);
}
export function ToastTitle({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return <div className={cn("font-medium text-foreground", className)} {...props} />;
}
export function ToastDescription({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return <div className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
export function ToastClose({ className, ...props }: ComponentPropsWithoutRef<"button">) {
return (
<button
type="button"
className={cn(
"ml-auto rounded-sm p-1 text-muted-foreground opacity-80 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
className,
)}
{...props}
>
<X className="h-4 w-4" />
<span className="sr-only">Bildirimi kapat</span>
</button>
);
}
export function ToastViewport({ className, ...props }: ComponentPropsWithoutRef<"div">) {
return (
<div
className={cn(
"fixed bottom-4 right-4 z-[80] flex w-[min(calc(100vw-2rem),24rem)] flex-col gap-2",
className,
)}
{...props}
/>
);
}
-35
View File
@@ -1,35 +0,0 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}