diff --git a/frontend/actions/notifications.ts b/frontend/actions/notifications.ts index a71a6570..ce4daa06 100644 --- a/frontend/actions/notifications.ts +++ b/frontend/actions/notifications.ts @@ -1,6 +1,6 @@ 'use server'; -import { and,desc, eq } from 'drizzle-orm'; +import { and, desc, eq } from 'drizzle-orm'; import { db } from '@/db'; import { notifications } from '@/db/schema/notifications'; @@ -9,7 +9,7 @@ import { getCurrentUser } from '@/lib/auth'; export async function getNotifications() { const session = await getCurrentUser(); if (!session) return []; - + try { const data = await db.query.notifications.findMany({ where: eq(notifications.userId, session.id), @@ -53,7 +53,12 @@ export async function markAllAsRead() { await db .update(notifications) .set({ isRead: true }) - .where(and(eq(notifications.userId, session.id), eq(notifications.isRead, false))); + .where( + and( + eq(notifications.userId, session.id), + eq(notifications.isRead, false) + ) + ); return { success: true }; } catch (error) { diff --git a/frontend/actions/profile.ts b/frontend/actions/profile.ts index 73544035..1d3687aa 100644 --- a/frontend/actions/profile.ts +++ b/frontend/actions/profile.ts @@ -22,7 +22,7 @@ export async function updateName(formData: FormData) { try { await updateUser(session.id, { name: name.trim() }); - + // Create notification const tNotify = await getTranslations('notifications.account'); await createNotification({ @@ -62,7 +62,7 @@ export async function updatePassword(formData: FormData) { const { db } = await import('@/db'); const { users } = await import('@/db/schema/users'); const { eq } = await import('drizzle-orm'); - + const dbUser = await db.query.users.findFirst({ where: eq(users.id, session.id), }); diff --git a/frontend/app/[locale]/admin/shop/orders/[id]/ShippingActions.tsx b/frontend/app/[locale]/admin/shop/orders/[id]/ShippingActions.tsx index 9b66b6c5..feeedb7f 100644 --- a/frontend/app/[locale]/admin/shop/orders/[id]/ShippingActions.tsx +++ b/frontend/app/[locale]/admin/shop/orders/[id]/ShippingActions.tsx @@ -19,7 +19,8 @@ function actionEnabled(args: { }): boolean { if (args.action === 'retry_label_creation') { return ( - args.shipmentStatus === 'failed' || args.shipmentStatus === 'needs_attention' + args.shipmentStatus === 'failed' || + args.shipmentStatus === 'needs_attention' ); } if (args.action === 'mark_shipped') { diff --git a/frontend/app/[locale]/dashboard/page.tsx b/frontend/app/[locale]/dashboard/page.tsx index f867603a..d3123f83 100644 --- a/frontend/app/[locale]/dashboard/page.tsx +++ b/frontend/app/[locale]/dashboard/page.tsx @@ -165,7 +165,7 @@ export default async function DashboardPage({ const outlineBtnStyles = 'inline-flex items-center justify-center rounded-full border border-gray-200/50 bg-white/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-gray-700 backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-white/20 hover:shadow-md hover:border-gray-300 dark:border-white/10 dark:bg-neutral-900/40 dark:text-gray-200 dark:hover:bg-neutral-800/80 dark:hover:border-white/20'; - const sponsorBtnStyles = + const sponsorBtnStyles = 'group relative inline-flex items-center justify-center gap-2 rounded-full border border-(--accent-primary)/30 bg-(--accent-primary)/10 px-6 py-2.5 text-sm font-semibold tracking-wide text-(--accent-primary) backdrop-blur-md transition-all hover:-translate-y-0.5 hover:bg-(--accent-primary)/20 hover:shadow-[0_4px_12px_rgba(var(--accent-primary-rgb),0.2)] hover:border-(--accent-primary)/50 dark:border-(--accent-primary)/20 dark:bg-(--accent-primary)/5 dark:hover:bg-(--accent-primary)/20 dark:hover:border-(--accent-primary)/40 dark:hover:shadow-[0_4px_15px_rgba(var(--accent-primary-rgb),0.3)] overflow-hidden'; return ( @@ -188,7 +188,7 @@ export default async function DashboardPage({ href="#feedback" className={`group flex items-center gap-2 ${outlineBtnStyles}`} > - + {t('supportLink')} {/* Subtle gradient glow background effect */}
- + - {isMatchedSponsor ? t('profile.supportAgain') : t('profile.becomeSponsor')} + {isMatchedSponsor + ? t('profile.supportAgain') + : t('profile.becomeSponsor')}
@@ -216,9 +218,13 @@ export default async function DashboardPage({ totalAttempts={totalAttempts} globalRank={globalRank} /> -
+
- +
diff --git a/frontend/app/[locale]/quizzes/page.tsx b/frontend/app/[locale]/quizzes/page.tsx index 7706683a..cba8b17b 100644 --- a/frontend/app/[locale]/quizzes/page.tsx +++ b/frontend/app/[locale]/quizzes/page.tsx @@ -3,9 +3,7 @@ import { getTranslations } from 'next-intl/server'; import QuizzesSection from '@/components/quiz/QuizzesSection'; import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground'; -import { - getActiveQuizzes, -} from '@/db/queries/quizzes/quiz'; +import { getActiveQuizzes } from '@/db/queries/quizzes/quiz'; type PageProps = { params: Promise<{ locale: string }> }; @@ -21,7 +19,7 @@ export async function generateMetadata({ }; } -export const revalidate = 300 +export const revalidate = 300; export default async function QuizzesPage({ params }: PageProps) { const { locale } = await params; diff --git a/frontend/app/api/quiz/progress/route.ts b/frontend/app/api/quiz/progress/route.ts index 69819f4f..e522b03c 100644 --- a/frontend/app/api/quiz/progress/route.ts +++ b/frontend/app/api/quiz/progress/route.ts @@ -9,13 +9,19 @@ export async function GET() { const user = await getCurrentUser(); if (!user?.id) { - return NextResponse.json({}, { - headers: { 'Cache-Control': 'no-store' }, - }); + return NextResponse.json( + {}, + { + headers: { 'Cache-Control': 'no-store' }, + } + ); } const rawProgress = await getUserQuizzesProgress(user.id); - const progressMap: Record = {}; + const progressMap: Record< + string, + { bestScore: number; totalQuestions: number; attemptsCount: number } + > = {}; for (const [quizId, progress] of rawProgress) { progressMap[quizId] = { diff --git a/frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts b/frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts index 40f97d0f..edddb7a4 100644 --- a/frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts +++ b/frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts @@ -11,10 +11,7 @@ import { import { logError, logWarn } from '@/lib/logging'; import { requireAdminCsrf } from '@/lib/security/admin-csrf'; import { guardBrowserSameOrigin } from '@/lib/security/origin'; -import { - InvalidPayloadError, - OrderNotFoundError, -} from '@/lib/services/errors'; +import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors'; import { offerIntlQuote } from '@/lib/services/shop/quotes'; import { intlQuoteOfferPayloadSchema, @@ -133,7 +130,10 @@ export async function POST( ); } if (error instanceof AdminForbiddenError) { - return noStoreJson({ code: error.code, message: 'Forbidden.' }, { status: 403 }); + return noStoreJson( + { code: error.code, message: 'Forbidden.' }, + { status: 403 } + ); } if (error instanceof OrderNotFoundError) { return noStoreJson({ code: error.code }, { status: 404 }); diff --git a/frontend/app/api/shop/admin/orders/[id]/shipping/route.ts b/frontend/app/api/shop/admin/orders/[id]/shipping/route.ts index 0dc025bb..169df721 100644 --- a/frontend/app/api/shop/admin/orders/[id]/shipping/route.ts +++ b/frontend/app/api/shop/admin/orders/[id]/shipping/route.ts @@ -144,11 +144,17 @@ export async function POST( } if (error instanceof AdminUnauthorizedError) { - return noStoreJson({ code: error.code, message: 'Unauthorized.' }, { status: 401 }); + return noStoreJson( + { code: error.code, message: 'Unauthorized.' }, + { status: 401 } + ); } if (error instanceof AdminForbiddenError) { - return noStoreJson({ code: error.code, message: 'Forbidden.' }, { status: 403 }); + return noStoreJson( + { code: error.code, message: 'Forbidden.' }, + { status: 403 } + ); } if (error instanceof ShippingAdminActionError) { diff --git a/frontend/app/api/shop/internal/shipping/shipments/run/route.ts b/frontend/app/api/shop/internal/shipping/shipments/run/route.ts index 417be95f..9c647e52 100644 --- a/frontend/app/api/shop/internal/shipping/shipments/run/route.ts +++ b/frontend/app/api/shop/internal/shipping/shipments/run/route.ts @@ -5,7 +5,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; import { requireInternalJanitorAuth } from '@/lib/auth/internal-janitor'; -import { getNovaPoshtaConfig, getShopShippingFlags, NovaPoshtaConfigError } from '@/lib/env/nova-poshta'; +import { + getNovaPoshtaConfig, + getShopShippingFlags, + NovaPoshtaConfigError, +} from '@/lib/env/nova-poshta'; import { logError, logInfo, logWarn } from '@/lib/logging'; import { guardNonBrowserFailClosed } from '@/lib/security/origin'; import { @@ -87,7 +91,8 @@ async function readJsonBodyOrDefault(request: NextRequest): Promise { } export async function POST(request: NextRequest) { - const requestId = request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); + const requestId = + request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); const runId = crypto.randomUUID(); const baseMeta = { requestId, diff --git a/frontend/app/api/shop/orders/[id]/quote/accept/route.ts b/frontend/app/api/shop/orders/[id]/quote/accept/route.ts index 35ab666d..0ca0b323 100644 --- a/frontend/app/api/shop/orders/[id]/quote/accept/route.ts +++ b/frontend/app/api/shop/orders/[id]/quote/accept/route.ts @@ -4,10 +4,7 @@ import { NextRequest } from 'next/server'; import { logError, logWarn } from '@/lib/logging'; import { guardBrowserSameOrigin } from '@/lib/security/origin'; -import { - InvalidPayloadError, - OrderNotFoundError, -} from '@/lib/services/errors'; +import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors'; import { authorizeOrderMutationAccess } from '@/lib/services/shop/order-access'; import { acceptIntlQuote } from '@/lib/services/shop/quotes'; import { diff --git a/frontend/app/api/shop/orders/[id]/quote/decline/route.ts b/frontend/app/api/shop/orders/[id]/quote/decline/route.ts index b7b3a204..8da775ef 100644 --- a/frontend/app/api/shop/orders/[id]/quote/decline/route.ts +++ b/frontend/app/api/shop/orders/[id]/quote/decline/route.ts @@ -4,10 +4,7 @@ import { NextRequest } from 'next/server'; import { logError, logWarn } from '@/lib/logging'; import { guardBrowserSameOrigin } from '@/lib/security/origin'; -import { - InvalidPayloadError, - OrderNotFoundError, -} from '@/lib/services/errors'; +import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors'; import { authorizeOrderMutationAccess } from '@/lib/services/shop/order-access'; import { declineIntlQuote } from '@/lib/services/shop/quotes'; import { diff --git a/frontend/app/api/shop/orders/[id]/quote/request/route.ts b/frontend/app/api/shop/orders/[id]/quote/request/route.ts index 3d2c9efa..87207f21 100644 --- a/frontend/app/api/shop/orders/[id]/quote/request/route.ts +++ b/frontend/app/api/shop/orders/[id]/quote/request/route.ts @@ -4,10 +4,7 @@ import { NextRequest } from 'next/server'; import { logError, logWarn } from '@/lib/logging'; import { guardBrowserSameOrigin } from '@/lib/security/origin'; -import { - InvalidPayloadError, - OrderNotFoundError, -} from '@/lib/services/errors'; +import { InvalidPayloadError, OrderNotFoundError } from '@/lib/services/errors'; import { authorizeOrderMutationAccess } from '@/lib/services/shop/order-access'; import { requestIntlQuote } from '@/lib/services/shop/quotes'; import { orderIdParamSchema } from '@/lib/validation/shop'; @@ -20,11 +17,12 @@ export async function POST( ) { const raw = request.headers.get('x-request-id'); const candidateRequestId = raw?.trim() ?? ''; - const requestId = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( - candidateRequestId - ) - ? candidateRequestId - : crypto.randomUUID(); + const requestId = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + candidateRequestId + ) + ? candidateRequestId + : crypto.randomUUID(); const baseMeta = { requestId, route: request.nextUrl.pathname, diff --git a/frontend/app/api/shop/orders/[id]/returns/route.ts b/frontend/app/api/shop/orders/[id]/returns/route.ts index c16f940d..8997e064 100644 --- a/frontend/app/api/shop/orders/[id]/returns/route.ts +++ b/frontend/app/api/shop/orders/[id]/returns/route.ts @@ -9,7 +9,10 @@ import { getCurrentUser } from '@/lib/auth'; import { logError, logWarn } from '@/lib/logging'; import { guardBrowserSameOrigin } from '@/lib/security/origin'; import { InvalidPayloadError } from '@/lib/services/errors'; -import { createReturnRequest, listOrderReturns } from '@/lib/services/shop/returns'; +import { + createReturnRequest, + listOrderReturns, +} from '@/lib/services/shop/returns'; import { orderIdParamSchema } from '@/lib/validation/shop'; import { createReturnPayloadSchema } from '@/lib/validation/shop-returns'; @@ -118,7 +121,8 @@ export async function POST( return noStoreJson( { code: 'EXCHANGES_NOT_SUPPORTED', - message: 'Exchanges are not supported. Please create a return refund request.', + message: + 'Exchanges are not supported. Please create a return refund request.', }, 422 ); diff --git a/frontend/app/api/shop/orders/[id]/status/route.ts b/frontend/app/api/shop/orders/[id]/status/route.ts index 51b4e701..ce8ca9e4 100644 --- a/frontend/app/api/shop/orders/[id]/status/route.ts +++ b/frontend/app/api/shop/orders/[id]/status/route.ts @@ -63,13 +63,11 @@ export async function GET( const user = await getCurrentUser(); let authorized = false; let accessByStatusToken = false; - let tokenAuditSeed: - | { - nonce: string; - iat: number; - exp: number; - } - | null = null; + let tokenAuditSeed: { + nonce: string; + iat: number; + exp: number; + } | null = null; if (user) { const isAdmin = user.role === 'admin'; @@ -208,17 +206,6 @@ export async function GET( return noStoreJson(liteOrder, { status: 200 }); } - if (responseMode === 'lite') { - const liteOrder = await getOrderStatusLiteSummary(orderId); - logInfo('order_status_responded', { - requestId, - orderId, - responseMode, - durationMs: Date.now() - startedAtMs, - }); - return noStoreJson(liteOrder, { status: 200 }); - } - const order = await getOrderSummary(orderId); const attempt = await getOrderAttemptSummary(orderId); logInfo('order_status_responded', { diff --git a/frontend/app/api/shop/shipping/methods/route.ts b/frontend/app/api/shop/shipping/methods/route.ts index ceb538c6..610cc8d8 100644 --- a/frontend/app/api/shop/shipping/methods/route.ts +++ b/frontend/app/api/shop/shipping/methods/route.ts @@ -5,7 +5,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { getShopShippingFlags } from '@/lib/env/nova-poshta'; import { readPositiveIntEnv } from '@/lib/env/readPositiveIntEnv'; import { logError, logWarn } from '@/lib/logging'; -import { enforceRateLimit, getRateLimitSubject, rateLimitResponse } from '@/lib/security/rate-limit'; +import { + enforceRateLimit, + getRateLimitSubject, + rateLimitResponse, +} from '@/lib/security/rate-limit'; import { resolveCurrencyFromLocale } from '@/lib/shop/currency'; import { resolveRequestLocale } from '@/lib/shop/request-locale'; import { resolveShippingAvailability } from '@/lib/services/shop/shipping/availability'; @@ -23,7 +27,13 @@ type ShippingMethod = { provider: 'nova_poshta'; methodCode: 'NP_WAREHOUSE' | 'NP_LOCKER' | 'NP_COURIER'; title: string; - requiredFields: Array<'cityRef' | 'warehouseRef' | 'addressLine1' | 'recipientName' | 'recipientPhone'>; + requiredFields: Array< + | 'cityRef' + | 'warehouseRef' + | 'addressLine1' + | 'recipientName' + | 'recipientPhone' + >; }; function cachedJson(body: unknown, requestId: string) { @@ -54,25 +64,41 @@ function getMethods(): ShippingMethod[] { provider: 'nova_poshta', methodCode: 'NP_WAREHOUSE', title: 'Nova Poshta warehouse', - requiredFields: ['cityRef', 'warehouseRef', 'recipientName', 'recipientPhone'], + requiredFields: [ + 'cityRef', + 'warehouseRef', + 'recipientName', + 'recipientPhone', + ], }, { provider: 'nova_poshta', methodCode: 'NP_LOCKER', title: 'Nova Poshta parcel locker', - requiredFields: ['cityRef', 'warehouseRef', 'recipientName', 'recipientPhone'], + requiredFields: [ + 'cityRef', + 'warehouseRef', + 'recipientName', + 'recipientPhone', + ], }, { provider: 'nova_poshta', methodCode: 'NP_COURIER', title: 'Nova Poshta courier', - requiredFields: ['cityRef', 'addressLine1', 'recipientName', 'recipientPhone'], + requiredFields: [ + 'cityRef', + 'addressLine1', + 'recipientName', + 'recipientPhone', + ], }, ]; } export async function GET(request: NextRequest) { - const requestId = request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); + const requestId = + request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); const baseMeta = { requestId, route: request.nextUrl.pathname, diff --git a/frontend/app/api/shop/shipping/np/cities/route.ts b/frontend/app/api/shop/shipping/np/cities/route.ts index 83754c4a..36d9259a 100644 --- a/frontend/app/api/shop/shipping/np/cities/route.ts +++ b/frontend/app/api/shop/shipping/np/cities/route.ts @@ -2,10 +2,17 @@ import crypto from 'node:crypto'; import { NextRequest, NextResponse } from 'next/server'; -import { getShopShippingFlags, NovaPoshtaConfigError } from '@/lib/env/nova-poshta'; +import { + getShopShippingFlags, + NovaPoshtaConfigError, +} from '@/lib/env/nova-poshta'; import { readPositiveIntEnv } from '@/lib/env/readPositiveIntEnv'; import { logError, logWarn } from '@/lib/logging'; -import { enforceRateLimit, getRateLimitSubject, rateLimitResponse } from '@/lib/security/rate-limit'; +import { + enforceRateLimit, + getRateLimitSubject, + rateLimitResponse, +} from '@/lib/security/rate-limit'; import { resolveShippingAvailability } from '@/lib/services/shop/shipping/availability'; import { sanitizeShippingErrorForLog, @@ -39,7 +46,8 @@ function noStoreJson(body: unknown, requestId: string, status = 200) { } export async function GET(request: NextRequest) { - const requestId = request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); + const requestId = + request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); const baseMeta = { requestId, route: request.nextUrl.pathname, diff --git a/frontend/app/api/shop/shipping/np/warehouses/route.ts b/frontend/app/api/shop/shipping/np/warehouses/route.ts index e0dff3c5..29a6efa0 100644 --- a/frontend/app/api/shop/shipping/np/warehouses/route.ts +++ b/frontend/app/api/shop/shipping/np/warehouses/route.ts @@ -2,10 +2,17 @@ import crypto from 'node:crypto'; import { NextRequest, NextResponse } from 'next/server'; -import { getShopShippingFlags, NovaPoshtaConfigError } from '@/lib/env/nova-poshta'; +import { + getShopShippingFlags, + NovaPoshtaConfigError, +} from '@/lib/env/nova-poshta'; import { readPositiveIntEnv } from '@/lib/env/readPositiveIntEnv'; import { logError, logWarn } from '@/lib/logging'; -import { enforceRateLimit, getRateLimitSubject, rateLimitResponse } from '@/lib/security/rate-limit'; +import { + enforceRateLimit, + getRateLimitSubject, + rateLimitResponse, +} from '@/lib/security/rate-limit'; import { resolveShippingAvailability } from '@/lib/services/shop/shipping/availability'; import { sanitizeShippingErrorForLog, @@ -39,14 +46,18 @@ function noStoreJson(body: unknown, requestId: string, status = 200) { } export async function GET(request: NextRequest) { - const requestId = request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); + const requestId = + request.headers.get('x-request-id')?.trim() || crypto.randomUUID(); const baseMeta = { requestId, route: request.nextUrl.pathname, method: request.method, }; - const limit = readPositiveIntEnv('SHOP_SHIPPING_WAREHOUSES_RATE_LIMIT_MAX', 60); + const limit = readPositiveIntEnv( + 'SHOP_SHIPPING_WAREHOUSES_RATE_LIMIT_MAX', + 60 + ); const windowSeconds = readPositiveIntEnv( 'SHOP_SHIPPING_WAREHOUSES_RATE_LIMIT_WINDOW_SECONDS', 60 diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e13ece48..0594718f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -231,10 +231,12 @@ html.is-scrolling .dark { .qa-accordion-item { position: relative; overflow: hidden; - background-image: linear-gradient(90deg, - transparent 0%, - transparent 54%, - var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%); + background-image: linear-gradient( + 90deg, + transparent 0%, + transparent 54%, + var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100% + ); } .qa-accordion-item:hover, @@ -298,30 +300,33 @@ html.is-scrolling .dark { } @keyframes wave-clip { - 0%, 100% { - clip-path: polygon(0% 50%, - 15% 48%, - 32% 52%, - 54% 60%, - 70% 62%, - 84% 60%, - 100% 55%, - 100% 100%, - 0% 100%); + clip-path: polygon( + 0% 50%, + 15% 48%, + 32% 52%, + 54% 60%, + 70% 62%, + 84% 60%, + 100% 55%, + 100% 100%, + 0% 100% + ); } 50% { - clip-path: polygon(0% 65%, - 16% 70%, - 34% 72%, - 51% 68%, - 67% 58%, - 84% 52%, - 100% 48%, - 100% 100%, - 0% 100%); + clip-path: polygon( + 0% 65%, + 16% 70%, + 34% 72%, + 51% 68%, + 67% 58%, + 84% 52%, + 100% 48%, + 100% 100%, + 0% 100% + ); } } @@ -341,7 +346,8 @@ html.is-scrolling .dark { } 50% { - transform: translate(var(--card-x, 0), var(--card-y, 0)) scale(1.05) rotate(calc(var(--card-rotate, 0deg) + var(--card-rotate-offset, 0deg))); + transform: translate(var(--card-x, 0), var(--card-y, 0)) scale(1.05) + rotate(calc(var(--card-rotate, 0deg) + var(--card-rotate-offset, 0deg))); } 100% { @@ -422,7 +428,6 @@ html.is-scrolling .dark { } @keyframes float { - 0%, 100% { transform: translateY(0); @@ -491,12 +496,16 @@ html.is-scrolling .dark { 0 0 0 2px rgba(0, 0, 0, 0.4), 0 0 0 7px rgba(0, 0, 0, 0.1), 0 22px 60px rgba(0, 0, 0, 0.28); - --shop-hero-btn-success-bg: color-mix(in oklab, - var(--shop-hero-btn-bg) 88%, - white); - --shop-hero-btn-success-bg-hover: color-mix(in oklab, - var(--shop-hero-btn-bg) 80%, - white); + --shop-hero-btn-success-bg: color-mix( + in oklab, + var(--shop-hero-btn-bg) 88%, + white + ); + --shop-hero-btn-success-bg-hover: color-mix( + in oklab, + var(--shop-hero-btn-bg) 80%, + white + ); --shop-hero-btn-success-shadow: 0 22px 60px rgba(0, 0, 0, 0.25); --shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(0, 0, 0, 0.32); } @@ -549,12 +558,16 @@ html.is-scrolling .dark { 0 0 0 2px rgba(255, 45, 85, 0.7), 0 0 0 7px rgba(255, 45, 85, 0.22), 0 22px 70px rgba(255, 45, 85, 0.38); - --shop-hero-btn-success-bg: color-mix(in oklab, - var(--accent-primary) 82%, - black); - --shop-hero-btn-success-bg-hover: color-mix(in oklab, - var(--accent-primary) 72%, - black); + --shop-hero-btn-success-bg: color-mix( + in oklab, + var(--accent-primary) 82%, + black + ); + --shop-hero-btn-success-bg-hover: color-mix( + in oklab, + var(--accent-primary) 72%, + black + ); --shop-hero-btn-success-shadow: 0 22px 60px rgba(255, 45, 85, 0.45); --shop-hero-btn-success-shadow-hover: 0 28px 80px rgba(255, 45, 85, 0.6); } @@ -614,11 +627,10 @@ html.is-scrolling .dark { } @media (prefers-reduced-motion: reduce) { - .animate-float, .animate-spin-slow, .animate-spin-slower, .animate-dash-flow { animation: none !important; } -} \ No newline at end of file +} diff --git a/frontend/components/admin/quiz/QuestionEditor.tsx b/frontend/components/admin/quiz/QuestionEditor.tsx index 3ebfaa97..a85d350d 100644 --- a/frontend/components/admin/quiz/QuestionEditor.tsx +++ b/frontend/components/admin/quiz/QuestionEditor.tsx @@ -502,7 +502,9 @@ export function QuestionEditor({ @@ -310,10 +369,10 @@ export function ActivityHeatmapCard({ attempts, locale, currentStreak }: Activit animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: -4 }} transition={{ duration: 0.15, ease: 'easeOut' }} - className="absolute right-0 top-full mt-2 w-44 overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-xl shadow-black/10 dark:border-neutral-700/60 dark:bg-neutral-900 dark:shadow-black/40" + className="absolute top-full right-0 mt-2 w-44 overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-xl shadow-black/10 dark:border-neutral-700/60 dark:bg-neutral-900 dark:shadow-black/40" > -
- {periodOptions.map((option) => { +
+ {periodOptions.map(option => { const isSelected = periodOffset === option.value; return ( @@ -347,173 +416,255 @@ export function ActivityHeatmapCard({ attempts, locale, currentStreak }: Activit
-
-
- - {monthsData.map((m, i) => ( - - {m.monthStr} - - ))} - -
- -
-
-
- - - - - - - - - - - - - {monthsData.map((m, i) => ( - - ))} - {traces.map((tr, i) => ( - - ))} - - {nodes.map((n, i) => { - const active = n.count > 0; - return ( - { - const rect = (e.currentTarget as Element).getBoundingClientRect(); - setTooltip({ count: n.count, date: n.date, top: rect.top - 8, left: rect.left + rect.width / 2 }); - }} - onMouseLeave={() => setTooltip(null)} - /> - ); - })} - - {nodes.filter(n => n.isToday).map(n => ( - - ))} - -
+
+
+ + {monthsData.map((m, i) => ( + + {m.monthStr} + + ))} +
-
-
- - {tooltip && ( - -
-

- {tooltip.date.toLocaleDateString(locale, { weekday: 'short', month: 'short', day: 'numeric' })} -

-

- {tooltip.count === 0 - ? t('heatmapNoActivity') - : t('heatmapAttempts', { count: tooltip.count })} -

-
-
+
+
+ + + + + + + + + + + + + {monthsData.map((m, i) => ( + + ))} + {traces.map((tr, i) => ( + + ))} + + {nodes.map((n, i) => { + const active = n.count > 0; + return ( + { + const rect = ( + e.currentTarget as Element + ).getBoundingClientRect(); + setTooltip({ + count: n.count, + date: n.date, + top: rect.top - 8, + left: rect.left + rect.width / 2, + }); + }} + onMouseLeave={() => setTooltip(null)} + /> + ); + })} + + {nodes + .filter(n => n.isToday) + .map(n => ( + + ))} +
- - )} - - -
-
- {t('less')} -
- {[ - { count: 1, label: '1' }, - { count: 2, label: '2' }, - { count: 3, label: '3+' }, - ].map(l => ( - - - - ))}
- {t('more')}
-
- {totalActiveDays > 0 && ( - - - {t('heatmapActiveDays', { count: totalActiveDays })} - + + {tooltip && ( + +
+

+ {tooltip.date.toLocaleDateString(locale, { + weekday: 'short', + month: 'short', + day: 'numeric', + })} +

+

+ {tooltip.count === 0 + ? t('heatmapNoActivity') + : t('heatmapAttempts', { count: tooltip.count })} +

+
+
+
+
+ )} + + +
+
+ {t('less')} +
+ {[ + { count: 1, label: '1' }, + { count: 2, label: '2' }, + { count: 3, label: '3+' }, + ].map(l => ( + + + + ))} +
+ {t('more')} +
+ +
+ {totalActiveDays > 0 && ( + + + + {t('heatmapActiveDays', { count: totalActiveDays })} + + + )} +
-
); } diff --git a/frontend/components/dashboard/ExplainedTermsCard.tsx b/frontend/components/dashboard/ExplainedTermsCard.tsx index e6c0033b..a9a8bc2d 100644 --- a/frontend/components/dashboard/ExplainedTermsCard.tsx +++ b/frontend/components/dashboard/ExplainedTermsCard.tsx @@ -231,17 +231,15 @@ export function ExplainedTermsCard() { const hasHiddenTerms = hiddenTerms.length > 0; const cardStyles = 'dashboard-card flex flex-col p-6 sm:p-8 lg:p-10'; - const iconBoxStyles = 'shrink-0 rounded-xl bg-white/40 border border-white/20 shadow-xs backdrop-blur-xs p-3 dark:bg-white/5 dark:border-white/10'; + const iconBoxStyles = + 'shrink-0 rounded-xl bg-white/40 border border-white/20 shadow-xs backdrop-blur-xs p-3 dark:bg-white/5 dark:border-white/10'; return ( <>
-