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
@@ -22,7 +22,7 @@ type AnalyticsClientProps = {
data: AnalyticsData;
};
const COLORS = ["hsl(var(--primary))", "hsl(var(--destructive))", "#eab308", "#3b82f6", "#8b5cf6"];
const COLORS = ["var(--poyraz-primary)", "var(--poyraz-destructive)", "#eab308", "#3b82f6", "#8b5cf6"];
export function AnalyticsClient({ data }: AnalyticsClientProps) {
const router = useRouter();
@@ -98,8 +98,8 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<Tooltip
formatter={(value) => `${Number(value ?? 0)}`}
contentStyle={{
backgroundColor: 'hsl(var(--background))',
borderColor: 'hsl(var(--border))',
backgroundColor: 'var(--poyraz-background)',
borderColor: 'var(--poyraz-border)',
borderRadius: '0.375rem',
}}
/>
@@ -119,9 +119,9 @@ export function AnalyticsClient({ data }: AnalyticsClientProps) {
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={taskStatusData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} dx={-10} />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--poyraz-border)" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }} dx={-10} />
<Tooltip
cursor={{ fill: 'transparent' }}
content={({ active, payload, label }) => {
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function AnalyticsLoading() {
return (
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function ClientsLoading() {
return (
+11 -11
View File
@@ -111,18 +111,18 @@ export function DashboardClient({ data }: DashboardClientProps) {
{incomeTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={incomeTrendData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--poyraz-border)" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }}
dx={-10}
/>
<Tooltip
@@ -181,26 +181,26 @@ export function DashboardClient({ data }: DashboardClientProps) {
{moodTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={moodTrendData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--poyraz-border)" />
<XAxis
dataKey="date"
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }}
dy={10}
/>
<YAxis
domain={[0, 5]}
axisLine={false}
tickLine={false}
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
tick={{ fontSize: 12, fill: 'var(--poyraz-muted-foreground)' }}
width={30}
dx={-10}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--background))',
borderColor: 'hsl(var(--border))',
backgroundColor: 'var(--poyraz-background)',
borderColor: 'var(--poyraz-border)',
borderRadius: '0.375rem',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
}}
@@ -208,9 +208,9 @@ export function DashboardClient({ data }: DashboardClientProps) {
<Line
type="monotone"
dataKey="mood"
stroke="hsl(var(--primary))"
stroke="var(--poyraz-primary)"
strokeWidth={3}
dot={{ r: 4, fill: "hsl(var(--primary))", strokeWidth: 2, stroke: "hsl(var(--background))" }}
dot={{ r: 4, fill: "var(--poyraz-primary)", strokeWidth: 2, stroke: "var(--poyraz-background)" }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
<Line
@@ -218,7 +218,7 @@ export function DashboardClient({ data }: DashboardClientProps) {
dataKey="energy"
stroke="#eab308"
strokeWidth={3}
dot={{ r: 4, fill: "#eab308", strokeWidth: 2, stroke: "hsl(var(--background))" }}
dot={{ r: 4, fill: "#eab308", strokeWidth: 2, stroke: "var(--poyraz-background)" }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
</LineChart>
+2 -2
View File
@@ -138,12 +138,12 @@ export function JournalClient({ logs }: JournalClientProps) {
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ left: -16, right: 16, top: 12, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<CartesianGrid strokeDasharray="3 3" stroke="var(--poyraz-border)" />
<XAxis dataKey="date" tickLine={false} axisLine={false} fontSize={12} />
<YAxis domain={[1, 5]} tickCount={5} tickLine={false} axisLine={false} fontSize={12} />
<Tooltip
contentStyle={{
border: "1px solid hsl(var(--border))",
border: "1px solid var(--poyraz-border)",
borderRadius: 4,
boxShadow: "0 10px 30px rgba(15, 23, 42, 0.08)",
}}
+8
View File
@@ -1,5 +1,6 @@
import { DashboardShell } from "@/components/layout/dashboard-shell";
import { requireFreelancer } from "@/server/auth/session";
import { getPublicBranding } from "@/server/branding/runtime";
export default async function DashboardLayout({
children,
@@ -7,6 +8,7 @@ export default async function DashboardLayout({
children: React.ReactNode;
}>) {
const { user, profile } = await requireFreelancer();
const branding = getPublicBranding();
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Neta Kullanıcısı";
const shortName =
@@ -20,6 +22,12 @@ export default async function DashboardLayout({
return (
<DashboardShell
branding={{
applicationName: branding.applicationName,
organizationName: branding.organizationName,
lightLogoUrl: branding.lightLogoUrl,
darkLogoUrl: branding.darkLogoUrl,
}}
user={{
email: user.email,
displayName,
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function DashboardLoading() {
return (
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function ProjectDetailLoading() {
return (
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function ProjectsLoading() {
return (
+1 -2
View File
@@ -1,5 +1,4 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent } from "poyraz-ui/atoms";
import { Card, CardContent, Skeleton } from "poyraz-ui/atoms";
export default function TasksLoading() {
return (
+8 -120
View File
@@ -1,85 +1,13 @@
@import "tailwindcss";
@import "poyraz-ui/preset.css";
@source "../app/**/*.{js,ts,jsx,tsx,mdx}";
@source "../components/**/*.{js,ts,jsx,tsx,mdx}";
@source "../config/**/*.{js,ts,jsx,tsx,mdx}";
@theme inline {
--font-sans: var(--font-geist-sans);
--color-background: var(--background);
--color-background-dark: var(--background-dark);
--color-foreground: var(--foreground);
--color-surface: var(--surface);
--color-surface-raised: var(--surface-raised);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary-hover: var(--primary-hover);
--color-primary-pressed: var(--primary-pressed);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-hover: var(--accent-hover);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-hover: var(--destructive-hover);
--color-destructive-pressed: var(--destructive-pressed);
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-info: var(--info);
--color-border: var(--border);
--color-border-strong: var(--border-strong);
--color-input: var(--input);
--color-input-bg: var(--input-bg);
--color-ring: var(--ring);
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 8px;
}
@layer base {
:root {
--background: #f7f8fb;
--background-dark: #f8fafc;
--foreground: #101828;
--surface: #ffffff;
--surface-raised: #ffffff;
--card: #ffffff;
--card-foreground: #101828;
--popover: #ffffff;
--popover-foreground: #101828;
--primary: #c81e1e;
--primary-foreground: #ffffff;
--primary-hover: #a91b1b;
--primary-pressed: #861515;
--secondary: #eef2f7;
--secondary-foreground: #182230;
--muted: #eef2f7;
--muted-foreground: #667085;
--accent: #e6edf5;
--accent-hover: #d8e2ee;
--accent-foreground: #101828;
--destructive: #d92d20;
--destructive-hover: #b42318;
--destructive-pressed: #912018;
--destructive-foreground: #ffffff;
--success: #168a4a;
--info: #2563eb;
--border: #d9e0ea;
--border-strong: #b9c4d2;
--input: #b9c4d2;
--input-bg: #ffffff;
--overlay: rgba(15, 23, 42, 0.48);
--ring: #c81e1e;
--radius: 0.375rem;
--motion-duration-fast: 120ms;
--motion-duration-base: 180ms;
--poyraz-font-primary: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
}
* {
@@ -90,48 +18,8 @@
color-scheme: light;
}
:root[data-color-mode="dark"] {
:root.dark {
color-scheme: dark;
--background: #0b1018;
--background-dark: #0b1018;
--foreground: #f2f4f7;
--surface: #121926;
--surface-raised: #182230;
--card: #121926;
--card-foreground: #f2f4f7;
--popover: #182230;
--popover-foreground: #f2f4f7;
--secondary: #253044;
--secondary-foreground: #f2f4f7;
--muted: #253044;
--muted-foreground: #98a2b3;
--border: #344054;
--border-strong: #475467;
--input: #475467;
--input-bg: #121926;
}
@media (prefers-color-scheme: dark) {
:root[data-color-mode="system"] {
color-scheme: dark;
--background: #0b1018;
--background-dark: #0b1018;
--foreground: #f2f4f7;
--surface: #121926;
--surface-raised: #182230;
--card: #121926;
--card-foreground: #f2f4f7;
--popover: #182230;
--popover-foreground: #f2f4f7;
--secondary: #253044;
--secondary-foreground: #f2f4f7;
--muted: #253044;
--muted-foreground: #98a2b3;
--border: #344054;
--border-strong: #475467;
--input: #475467;
--input-bg: #121926;
}
}
body {
@@ -144,8 +32,8 @@
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px var(--input-bg) inset !important;
-webkit-text-fill-color: var(--foreground) !important;
-webkit-box-shadow: 0 0 0 30px var(--poyraz-surface) inset !important;
-webkit-text-fill-color: var(--poyraz-foreground) !important;
transition: background-color 5000s ease-in-out 0s;
}
}
@@ -153,7 +41,7 @@
@layer utilities {
* {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--primary) 45%, transparent) transparent;
scrollbar-color: color-mix(in srgb, var(--poyraz-primary) 45%, transparent) transparent;
}
*::-webkit-scrollbar,
@@ -169,7 +57,7 @@
*::-webkit-scrollbar-thumb,
.tiny-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--primary) 36%, transparent);
background: color-mix(in srgb, var(--poyraz-primary) 36%, transparent);
border: 2px solid transparent;
border-radius: 999px;
background-clip: padding-box;
@@ -177,7 +65,7 @@
*::-webkit-scrollbar-thumb:hover,
.tiny-scrollbar::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--primary) 58%, transparent);
background: color-mix(in srgb, var(--poyraz-primary) 58%, transparent);
background-clip: padding-box;
}
+12 -3
View File
@@ -5,9 +5,10 @@ import { acceptInvitation } from "./actions";
import { AuthPageShell } from "@/components/auth/auth-page-shell";
import { SubmitButton } from "@/components/auth/submit-button";
import { ErrorToaster } from "@/components/error-toaster";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/field";
import { Input, Label } from "poyraz-ui/atoms";
import { Alert, AlertDescription } from "poyraz-ui/molecules";
import { getPortalInvitationPreview } from "@/server/auth/invitations";
import { getPublicBranding } from "@/server/branding/runtime";
export const dynamic = "force-dynamic";
@@ -20,6 +21,7 @@ export default async function InvitationPage({
}) {
const { token } = await params;
const invitation = getPortalInvitationPreview(token);
const branding = getPublicBranding();
if (!invitation) {
notFound();
@@ -40,6 +42,11 @@ export default async function InvitationPage({
<>
{query.error && query.message ? <ErrorToaster message={query.message} /> : null}
<AuthPageShell
branding={{
applicationName: branding.applicationName,
lightLogoUrl: branding.lightLogoUrl,
darkLogoUrl: branding.darkLogoUrl,
}}
title="Müşteri portalına katıl"
description="Davet edilen hesabın için adını ve şifreni belirle."
form={
@@ -75,7 +82,9 @@ export default async function InvitationPage({
</SubmitButton>
</form>
) : (
<p className="text-sm text-muted-foreground">{unavailableMessage}</p>
<Alert variant="warning" appearance="soft">
<AlertDescription>{unavailableMessage}</AlertDescription>
</Alert>
)
}
secondaryAction={null}
+12 -2
View File
@@ -4,10 +4,17 @@ import "./globals.css";
import { Geist } from "next/font/google";
import { cn } from "@/lib/utils";
import { OfflineIndicator } from "@/components/ui/offline-indicator";
import { Toaster } from "@/components/ui/toast";
import { Toaster } from "poyraz-ui/molecules";
import { getPublicBranding } from "@/server/branding/runtime";
const geist = Geist({ subsets: ["latin"], variable: "--font-geist-sans" });
const colorModeScript = `(() => {
const root = document.documentElement;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const apply = () => root.classList.toggle("dark", root.dataset.colorMode === "dark" || (root.dataset.colorMode === "system" && media.matches));
apply();
if (root.dataset.colorMode === "system") media.addEventListener("change", apply);
})();`;
export function generateMetadata(): Metadata {
const branding = getPublicBranding();
@@ -42,10 +49,13 @@ export default function RootLayout({
style={branding.cssVariables as CSSProperties}
suppressHydrationWarning
>
<head>
<script dangerouslySetInnerHTML={{ __html: colorModeScript }} />
</head>
<body>
{children}
<OfflineIndicator />
<Toaster />
<Toaster closeButton richColors position="top-right" />
</body>
</html>
);
+12 -5
View File
@@ -3,9 +3,10 @@ import { AuthPageShell } from "@/components/auth/auth-page-shell";
import { ErrorToaster } from "@/components/error-toaster";
import { LockKeyhole, LogIn, Mail } from "lucide-react";
import Link from "next/link";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/field";
import { Input, Label } from "poyraz-ui/atoms";
import { Alert, AlertDescription } from "poyraz-ui/molecules";
import { SubmitButton } from "@/components/auth/submit-button";
import { getPublicBranding } from "@/server/branding/runtime";
export default async function LoginPage({
searchParams,
@@ -15,19 +16,25 @@ export default async function LoginPage({
const resolvedParams = await searchParams;
const error = resolvedParams?.error;
const message = resolvedParams?.message;
const branding = getPublicBranding();
return (
<>
{error && message && <ErrorToaster message={String(message)} />}
<AuthPageShell
branding={{
applicationName: branding.applicationName,
lightLogoUrl: branding.lightLogoUrl,
darkLogoUrl: branding.darkLogoUrl,
}}
title="Giriş yap"
description="Neta çalışma alanına erişmek için hesabına giriş yap."
form={
<form className="space-y-6">
{!error && message ? (
<p className="rounded-md border border-success/30 bg-success/5 p-3 text-sm text-foreground">
{String(message)}
</p>
<Alert variant="success" appearance="soft">
<AlertDescription>{String(message)}</AlertDescription>
</Alert>
) : null}
<div className="space-y-4">
<div className="space-y-2">
+8
View File
@@ -1,5 +1,6 @@
import { PortalShell } from "@/components/layout/portal-shell";
import { requireClientUser } from "@/server/auth/session";
import { getPublicBranding } from "@/server/branding/runtime";
export default async function PortalLayout({
children,
@@ -7,6 +8,7 @@ export default async function PortalLayout({
children: React.ReactNode;
}>) {
const { user, profile } = await requireClientUser();
const branding = getPublicBranding();
const displayName = profile.displayName || user.name || user.email.split("@")[0] || "Müşteri";
const shortName =
@@ -20,6 +22,12 @@ export default async function PortalLayout({
return (
<PortalShell
branding={{
applicationName: branding.applicationName,
organizationName: branding.organizationName,
lightLogoUrl: branding.lightLogoUrl,
darkLogoUrl: branding.darkLogoUrl,
}}
user={{
email: user.email,
displayName,
+8 -2
View File
@@ -5,9 +5,9 @@ import { getFirstFreelancerSetupState } from "@/server/auth/setup";
import { LockKeyhole, Mail, UserPlus } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/field";
import { Input, Label } from "poyraz-ui/atoms";
import { SubmitButton } from "@/components/auth/submit-button";
import { getPublicBranding } from "@/server/branding/runtime";
export const dynamic = "force-dynamic";
@@ -33,11 +33,17 @@ export default async function RegisterPage({
const resolvedParams = await searchParams;
const error = resolvedParams?.error;
const message = resolvedParams?.message;
const branding = getPublicBranding();
return (
<>
{error && message && <ErrorToaster message={String(message)} />}
<AuthPageShell
branding={{
applicationName: branding.applicationName,
lightLogoUrl: branding.lightLogoUrl,
darkLogoUrl: branding.darkLogoUrl,
}}
title="İlk admin hesabını oluştur"
description="Bu Neta çalışma alanının ilk yönetici hesabını oluştur."
form={