Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2c77e2a
(SP: 3) [Backend] add Nova Poshta shipping foundation + checkout pers…
liudmylasovetovs Feb 25, 2026
4dd23b2
(SP: 2) [Frontend] Reduce Vercel variable costs via caching and analy…
ViktorSvertoka Feb 25, 2026
cf43f36
fix(build): align Netlify Node version and remove SpeedInsights import
ViktorSvertoka Feb 26, 2026
e07941e
chore(release): bump version to 1.0.4
ViktorSvertoka Feb 26, 2026
006b39b
Merge remote-tracking branch 'origin/main' into develop
ViktorSvertoka Feb 26, 2026
6e3526f
(SP: 2) [Frontend] Remove [locale] layout force-dynamic and move auth…
ViktorSvertoka Feb 26, 2026
9130df1
(SP: 2) [Frontend] Reduce auth overhead and sync auth state across t…
ViktorSvertoka Feb 26, 2026
3a59b4e
(SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub s…
LesiaUKR Feb 26, 2026
d6697f4
(SP: 1) [Frontend] Fix quiz timer flash and card layout shift on quiz…
LesiaUKR Feb 26, 2026
fe667af
chore(release): v1.0.5
ViktorSvertoka Feb 26, 2026
b94a599
Merge branch 'main' into develop
ViktorSvertoka Feb 26, 2026
ffca8ca
(SP: 3)[Shop][DB] Reduce Neon compute: throttle janitor + relax check…
liudmylasovetovs Feb 27, 2026
bf6d333
(SP: 3) [SHOP] audit-driven e2e purchase readiness hardening (events,…
liudmylasovetovs Feb 28, 2026
fee0978
feat(ui): add devops/cloud category icons and styles (#379)
ViktorSvertoka Mar 1, 2026
aa5d692
chore(release): prepare v1.0.6 changelog
ViktorSvertoka Mar 1, 2026
5409e13
chore: bump version to 1.0.6
ViktorSvertoka Mar 1, 2026
e216a39
Merge branch 'main' into develop
ViktorSvertoka Mar 1, 2026
5a6d6d6
fix(orders): close missing brace in checkout shipping snapshot try block
ViktorSvertoka Mar 1, 2026
2d7a86a
fix(checkout): correct nested try/catch structure for shipping snapshot
ViktorSvertoka Mar 1, 2026
2856099
fix(order-status): remove stale responseMode lite branch
ViktorSvertoka Mar 1, 2026
d3fd98b
resolve conflicts by accepting current changes
ViktorSvertoka Mar 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions frontend/actions/notifications.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/actions/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
18 changes: 12 additions & 6 deletions frontend/app/[locale]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -188,7 +188,7 @@ export default async function DashboardPage({
href="#feedback"
className={`group flex items-center gap-2 ${outlineBtnStyles}`}
>
<MessageSquare className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white" />
<MessageSquare className="h-4 w-4 text-gray-400 transition-transform group-hover:-translate-y-0.5 group-hover:text-gray-600 dark:group-hover:text-white" />
{t('supportLink')}
</a>
<a
Expand All @@ -199,10 +199,12 @@ export default async function DashboardPage({
>
{/* Subtle gradient glow background effect */}
<div className="absolute inset-0 z-0 bg-linear-to-r from-transparent via-(--accent-primary)/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />

<span className="relative z-10 flex items-center gap-2">
<Heart className="h-4 w-4 transition-transform group-hover:scale-110 group-hover:fill-(--accent-primary)/20" />
{isMatchedSponsor ? t('profile.supportAgain') : t('profile.becomeSponsor')}
{isMatchedSponsor
? t('profile.supportAgain')
: t('profile.becomeSponsor')}
</span>
</a>
</div>
Expand All @@ -216,9 +218,13 @@ export default async function DashboardPage({
totalAttempts={totalAttempts}
globalRank={globalRank}
/>
<div id="stats" className="grid gap-8 scroll-mt-8 lg:grid-cols-2">
<div id="stats" className="grid scroll-mt-8 gap-8 lg:grid-cols-2">
<StatsCard stats={stats} attempts={lastAttempts} />
<ActivityHeatmapCard attempts={attempts} locale={locale} currentStreak={currentStreak} />
<ActivityHeatmapCard
attempts={attempts}
locale={locale}
currentStreak={currentStreak}
/>
</div>
</div>
<div className="mt-8">
Expand Down
6 changes: 2 additions & 4 deletions frontend/app/[locale]/quizzes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> };

Expand All @@ -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;
Expand Down
14 changes: 10 additions & 4 deletions frontend/app/api/quiz/progress/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { bestScore: number; totalQuestions: number; attemptsCount: number }> = {};
const progressMap: Record<
string,
{ bestScore: number; totalQuestions: number; attemptsCount: number }
> = {};

for (const [quizId, progress] of rawProgress) {
progressMap[quizId] = {
Expand Down
10 changes: 5 additions & 5 deletions frontend/app/api/shop/admin/orders/[id]/quote/offer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
Expand Down
10 changes: 8 additions & 2 deletions frontend/app/api/shop/admin/orders/[id]/shipping/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -87,7 +91,8 @@ async function readJsonBodyOrDefault(request: NextRequest): Promise<unknown> {
}

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,
Expand Down
5 changes: 1 addition & 4 deletions frontend/app/api/shop/orders/[id]/quote/accept/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 1 addition & 4 deletions frontend/app/api/shop/orders/[id]/quote/decline/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 7 additions & 9 deletions frontend/app/api/shop/orders/[id]/quote/request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions frontend/app/api/shop/orders/[id]/returns/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
);
Expand Down
23 changes: 5 additions & 18 deletions frontend/app/api/shop/orders/[id]/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', {
Expand Down
Loading