feat: add new UI components and improve existing ones
- Introduced `Field` and `Label` components for better form handling. - Refactored `Input` and `Textarea` components to use forward refs and improved styling. - Updated `PendingSubmitButton` to use the new `Button` component. - Enhanced `Skeleton` component for better layout handling. - Revamped `Toast` component to support live regions and improved accessibility. - Updated sidebar configuration to use typed icons. - Added phase 3 UI boundary checks to prevent usage of deprecated imports. - Implemented new authentication action handler for better cookie management. - Improved setup logic for freelancer accounts with repair functionality.
This commit is contained in:
+84
-46
@@ -1,60 +1,85 @@
|
||||
@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 {
|
||||
--poyraz-background: #ffffff;
|
||||
--poyraz-foreground: #101828;
|
||||
--poyraz-primary: #dc2626;
|
||||
--poyraz-primary-foreground: #ffffff;
|
||||
--poyraz-primary-hover: #b91c1c;
|
||||
--poyraz-primary-active: #991b1b;
|
||||
--poyraz-primary-muted: #fef2f2;
|
||||
--poyraz-primary-muted-foreground: #b91c1c;
|
||||
--poyraz-secondary: #f8fafc;
|
||||
--poyraz-secondary-foreground: #101828;
|
||||
--poyraz-muted: #f8fafc;
|
||||
--poyraz-muted-foreground: #667085;
|
||||
--poyraz-accent: #f1f5f9;
|
||||
--poyraz-accent-hover: #e2e8f0;
|
||||
--poyraz-accent-foreground: #101828;
|
||||
--poyraz-border: #e4e7ec;
|
||||
--poyraz-border-strong: #d0d5dd;
|
||||
--poyraz-input: #98a2b3;
|
||||
--poyraz-ring: #dc2626;
|
||||
--poyraz-card: #ffffff;
|
||||
--poyraz-card-foreground: #101828;
|
||||
|
||||
/* Legacy aliases kept until all prototype pages move to Poyraz UI. */
|
||||
--background: var(--poyraz-background);
|
||||
--background: #f7f8fb;
|
||||
--background-dark: #f8fafc;
|
||||
--foreground: var(--poyraz-foreground);
|
||||
--card: var(--poyraz-card);
|
||||
--card-foreground: var(--poyraz-card-foreground);
|
||||
--foreground: #101828;
|
||||
--surface: #ffffff;
|
||||
--surface-raised: #ffffff;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #101828;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: var(--poyraz-foreground);
|
||||
--primary: var(--poyraz-primary);
|
||||
--primary-foreground: var(--poyraz-primary-foreground);
|
||||
--primary-hover: var(--poyraz-primary-hover);
|
||||
--primary-pressed: var(--poyraz-primary-active);
|
||||
--secondary: var(--poyraz-secondary);
|
||||
--secondary-foreground: var(--poyraz-secondary-foreground);
|
||||
--muted: var(--poyraz-muted);
|
||||
--muted-foreground: var(--poyraz-muted-foreground);
|
||||
--accent: var(--poyraz-accent);
|
||||
--accent-foreground: var(--poyraz-accent-foreground);
|
||||
--destructive: #ef4444;
|
||||
--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;
|
||||
--border: var(--poyraz-border);
|
||||
--input: var(--poyraz-input);
|
||||
--success: #168a4a;
|
||||
--info: #2563eb;
|
||||
--border: #d9e0ea;
|
||||
--border-strong: #b9c4d2;
|
||||
--input: #b9c4d2;
|
||||
--input-bg: #ffffff;
|
||||
--overlay: rgba(15, 23, 42, 0.48);
|
||||
--ring: var(--poyraz-ring);
|
||||
--ring: #c81e1e;
|
||||
--radius: 0.375rem;
|
||||
--motion-duration-fast: 120ms;
|
||||
--motion-duration-base: 180ms;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -67,6 +92,8 @@
|
||||
|
||||
body {
|
||||
@apply min-h-screen bg-background text-foreground antialiased;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
@@ -82,7 +109,7 @@
|
||||
@layer utilities {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--poyraz-primary) 45%, transparent) transparent;
|
||||
scrollbar-color: color-mix(in srgb, var(--primary) 45%, transparent) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar,
|
||||
@@ -98,7 +125,7 @@
|
||||
|
||||
*::-webkit-scrollbar-thumb,
|
||||
.tiny-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--poyraz-primary) 36%, transparent);
|
||||
background: color-mix(in srgb, var(--primary) 36%, transparent);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
@@ -106,7 +133,18 @@
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover,
|
||||
.tiny-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--poyraz-primary) 58%, transparent);
|
||||
background: color-mix(in srgb, var(--primary) 58%, transparent);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 1ms !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@ import type { Metadata, Viewport } from "next";
|
||||
import "./globals.css";
|
||||
import { Geist } from "next/font/google";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Toaster } from "poyraz-ui/molecules";
|
||||
import { OfflineIndicator } from "@/components/ui/offline-indicator";
|
||||
import { Toaster } from "@/components/ui/toast";
|
||||
|
||||
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" });
|
||||
const geist = Geist({ subsets: ["latin"], variable: "--font-geist-sans" });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Neta",
|
||||
|
||||
+42
-33
@@ -1,41 +1,32 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { auth } from '@/server/auth/auth'
|
||||
import { callAuthAction } from '@/server/auth/action-handler'
|
||||
import { getProfileByAuthUserId } from '@/server/auth/session'
|
||||
import { getFirstFreelancerSetupState, recordAuthAuditEvent } from '@/server/auth/setup'
|
||||
import {
|
||||
getFirstFreelancerSetupState,
|
||||
recordAuthAuditEvent,
|
||||
repairFirstFreelancerSetupForEmail,
|
||||
} from '@/server/auth/setup'
|
||||
import { getDefaultDisplayName, parseAuthCredentials } from '@/server/auth/validation'
|
||||
|
||||
const genericLoginError = 'E-posta veya şifre hatalı.'
|
||||
const genericLoginError = 'E-posta veya \u015fifre hatal\u0131.'
|
||||
type SignInEmailResult = Awaited<ReturnType<typeof auth.api.signInEmail>>
|
||||
type SignUpEmailResult = Awaited<ReturnType<typeof auth.api.signUpEmail>>
|
||||
|
||||
export async function login(formData: FormData) {
|
||||
const credentials = parseAuthCredentials(formData)
|
||||
let redirectTarget = '/'
|
||||
let result: SignInEmailResult
|
||||
|
||||
try {
|
||||
const result = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
rememberMe: true,
|
||||
},
|
||||
result = await callAuthAction<SignInEmailResult>('/sign-in/email', {
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
rememberMe: true,
|
||||
})
|
||||
const profile = getProfileByAuthUserId(result.user.id)
|
||||
|
||||
if (!profile || profile.disabled) {
|
||||
await auth.api.signOut({ headers: await headers() })
|
||||
await recordAuthAuditEvent({
|
||||
type: 'login_failed',
|
||||
authUserId: result.user.id,
|
||||
email: credentials.email,
|
||||
metadata: { reason: 'missing_or_disabled_profile' },
|
||||
})
|
||||
redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`)
|
||||
}
|
||||
|
||||
redirectTarget = profile.role === 'client' ? '/portal' : '/'
|
||||
} catch {
|
||||
await recordAuthAuditEvent({
|
||||
type: 'login_failed',
|
||||
@@ -45,6 +36,26 @@ export async function login(formData: FormData) {
|
||||
redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`)
|
||||
}
|
||||
|
||||
let profile = getProfileByAuthUserId(result.user.id)
|
||||
|
||||
if (!profile) {
|
||||
repairFirstFreelancerSetupForEmail(result.user.email)
|
||||
profile = getProfileByAuthUserId(result.user.id)
|
||||
}
|
||||
|
||||
if (!profile || profile.disabled) {
|
||||
await callAuthAction<{ success: boolean }>('/sign-out')
|
||||
await recordAuthAuditEvent({
|
||||
type: 'login_failed',
|
||||
authUserId: result.user.id,
|
||||
email: credentials.email,
|
||||
metadata: { reason: 'missing_or_disabled_profile' },
|
||||
})
|
||||
redirect(`/login?error=true&message=${encodeURIComponent(genericLoginError)}`)
|
||||
}
|
||||
|
||||
redirectTarget = profile.role === 'client' ? '/portal' : '/'
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect(redirectTarget)
|
||||
}
|
||||
@@ -59,7 +70,7 @@ export async function signup(formData: FormData) {
|
||||
if (!setupState.available) {
|
||||
redirect(
|
||||
`/login?error=true&message=${encodeURIComponent(
|
||||
'Kayıt kapalı. Bu Neta kurulumunda ilk freelancer hesabı zaten oluşturulmuş.',
|
||||
'Kay\u0131t kapal\u0131. Bu Neta kurulumunda ilk freelancer hesab\u0131 zaten olu\u015fturulmu\u015f.',
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
@@ -67,16 +78,14 @@ export async function signup(formData: FormData) {
|
||||
const credentials = parseAuthCredentials(formData)
|
||||
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
name: getDefaultDisplayName(credentials.email),
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
rememberMe: true,
|
||||
},
|
||||
await callAuthAction<SignUpEmailResult>('/sign-up/email', {
|
||||
name: getDefaultDisplayName(credentials.email),
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
rememberMe: true,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Kullanıcı oluşturulamadı.'
|
||||
const message = error instanceof Error ? error.message : 'Kullan\u0131c\u0131 olu\u015fturulamad\u0131.'
|
||||
redirect(`/register?error=true&message=${encodeURIComponent(message)}`)
|
||||
}
|
||||
|
||||
@@ -85,7 +94,7 @@ export async function signup(formData: FormData) {
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
await auth.api.signOut({ headers: await headers() })
|
||||
await callAuthAction<{ success: boolean }>('/sign-out')
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/login')
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ 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, Label } from "poyraz-ui/atoms";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/field";
|
||||
import { SubmitButton } from "@/components/auth/submit-button";
|
||||
|
||||
export default async function LoginPage({
|
||||
|
||||
@@ -5,7 +5,8 @@ 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, Label } from "poyraz-ui/atoms";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/field";
|
||||
import { SubmitButton } from "@/components/auth/submit-button";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
Reference in New Issue
Block a user