Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
82d01ea
chore: bump Node.js to 20 for Netlify
ViktorSvertoka Jan 31, 2026
b7c49a5
feat(md) add netlify status (#234)
ViktorSvertoka Jan 31, 2026
124bc04
(SP 2) [Shop UI] Unify storefront styles across components and intera…
liudmylasovetovs Jan 31, 2026
be2ddbb
Host (#237)
ViktorSvertoka Jan 31, 2026
0b726bf
Host (#238)
ViktorSvertoka Jan 31, 2026
d0a0586
(SP 1) [Shop UI] Add page metadata across shop routes (#239)
liudmylasovetovs Jan 31, 2026
c606ab4
(SP: 3) [Cache] Add Upstash Redis cache for Q&A (#241)
ViktorSvertoka Jan 31, 2026
d73f5fb
Fix Q&A Redis cache parsing for Upstash REST (#243)
ViktorSvertoka Feb 1, 2026
c23f2c0
feat(Blog):Adding pagination (#244)
KomrakovaAnna Feb 1, 2026
f1730f3
(SP:2) feat(api): clean up AI helper for Vercel & fix orders i18n (#…
TiZorii Feb 1, 2026
7a353d1
refactor(home): update button, cards, and online counter UI (#248)
YNazymko12 Feb 1, 2026
9ace93d
fix(api): enforce rate limiting (#246)
TiZorii Feb 1, 2026
fb63cc3
feat(Blog):formating text (#249)
KomrakovaAnna Feb 1, 2026
eaf3eb0
ref(files): refactoring code & bag fix (#250)
ViktorSvertoka Feb 1, 2026
2afb681
chore(release): v0.5.2
ViktorSvertoka Feb 1, 2026
2bcafdd
Lso/feat/shop design (#252)
liudmylasovetovs Feb 2, 2026
9ade51d
feat(i18n): add translations for blog categories, and UI components…
TiZorii Feb 2, 2026
9615bff
feat blog: fix for paddings on mobile (#254)
KomrakovaAnna Feb 2, 2026
0c12d6a
Merge remote-tracking branch 'origin/main' into develop
ViktorSvertoka Feb 2, 2026
2f9aca2
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
ViktorSvertoka Feb 2, 2026
7b8f89a
chore(lint): finalize ESLint + Prettier (#256)
ViktorSvertoka Feb 3, 2026
601e032
feat(leaderboard): finalize components and fix lint errors (#259)
AlinaRyabova Feb 3, 2026
4694478
(SP 1) [FIX] set up eslint/prettier + stabilize formatting workflow (…
liudmylasovetovs Feb 3, 2026
1ad1d7f
(SP: 5) [Quiz] Redis caching + guest session fix + cleanup (#263)
LesiaUKR Feb 3, 2026
6d678fc
chore: remove redis ttl for static quiz and qa caches (#265)
ViktorSvertoka Feb 3, 2026
10f1434
fix(layout): remove duplicate padding from quiz routes (#266)
LesiaUKR Feb 3, 2026
fc64c59
Merge branch 'main' into develop
ViktorSvertoka Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 0 additions & 10 deletions .prettierrc

This file was deleted.

5 changes: 4 additions & 1 deletion frontend/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"proseWrap": "always"
"proseWrap": "always",

"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx", "cn"]
}
29 changes: 21 additions & 8 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
This is a [Next.js](https://nextjs.org) project bootstrapped with
[`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

Expand All @@ -14,23 +15,35 @@ pnpm dev
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the
result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
You can start editing the page by modifying `app/page.tsx`. The page
auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
This project uses
[`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts)
to automatically optimize and load [Geist](https://vercel.com/font), a new font
family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
You can check out
[the Next.js GitHub repository](https://github.com/vercel/next.js) - your
feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
The easiest way to deploy your Next.js app is to use the
[Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Check out our
[Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying)
for more details.
38 changes: 30 additions & 8 deletions frontend/actions/quiz.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
'use server';

import { eq, inArray } from 'drizzle-orm';

import { db } from '@/db';
import { awardQuizPoints, calculateQuizPoints } from '@/db/queries/points';
import {
quizAttempts,
quizAttemptAnswers,
quizAnswers,
quizAttemptAnswers,
quizAttempts,
quizQuestions,
} from '@/db/schema/quiz';
import { awardQuizPoints,calculateQuizPoints } from '@/db/queries/points';
import { getCurrentUser } from '@/lib/auth';
import { eq, inArray, and } from 'drizzle-orm';

export interface UserAnswer {
questionId: string;
Expand Down Expand Up @@ -93,7 +94,8 @@ export async function submitQuizAttempt(
return { success: false, error: 'Unauthorized' };
}

const { userId, quizId, answers, violations, startedAt, completedAt } = input;
const { userId, quizId, answers, violations, startedAt, completedAt } =
input;

if (userId && userId !== session.id) {
return { success: false, error: 'User mismatch' };
Expand Down Expand Up @@ -186,9 +188,10 @@ export async function submitQuizAttempt(
};
}

const percentage = ((correctAnswersCount / questionIds.length) * 100).toFixed(
2
);
const percentage = (
(correctAnswersCount / questionIds.length) *
100
).toFixed(2);
const integrityScore = calculateIntegrityScore(violations);
const timeSpentSeconds = Math.floor(
(completedAtDate.getTime() - startedAtDate.getTime()) / 1000
Expand Down Expand Up @@ -252,3 +255,22 @@ export async function submitQuizAttempt(
};
}
}

export async function initializeQuizCache(
quizId: string
): Promise<{ success: boolean; error?: string }> {
try {
const { getOrCreateQuizAnswersCache } =
await import('@/lib/quiz/quiz-answers-redis');
const success = await getOrCreateQuizAnswersCache(quizId);

if (!success) {
return { success: false, error: 'Quiz not found' };
}

return { success: true };
} catch (error) {
console.error('Failed to initialize quiz cache:', error);
return { success: false, error: 'Internal server error' };
}
}
38 changes: 17 additions & 21 deletions frontend/app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import { getTranslations } from "next-intl/server"
import { getPlatformStats } from "@/lib/about/stats"
import { getSponsors } from "@/lib/about/github-sponsors"
import { getTranslations } from 'next-intl/server';

import { HeroSection } from "@/components/about/HeroSection"
import { TopicsSection } from "@/components/about/TopicsSection"
import { FeaturesSection } from "@/components/about/FeaturesSection"
import { PricingSection } from "@/components/about/PricingSection"
import { CommunitySection } from "@/components/about/CommunitySection"
import { CommunitySection } from '@/components/about/CommunitySection';
import { FeaturesSection } from '@/components/about/FeaturesSection';
import { HeroSection } from '@/components/about/HeroSection';
import { PricingSection } from '@/components/about/PricingSection';
import { TopicsSection } from '@/components/about/TopicsSection';
import { getSponsors } from '@/lib/about/github-sponsors';
import { getPlatformStats } from '@/lib/about/stats';

export async function generateMetadata() {
const t = await getTranslations("about")
const t = await getTranslations('about');
return {
title: t("metaTitle"),
description: t("metaDescription"),
}
title: t('metaTitle'),
description: t('metaDescription'),
};
}

export default async function AboutPage() {
const [stats, sponsors] = await Promise.all([
getPlatformStats(),
getSponsors()
])
getSponsors(),
]);

return (
<main className="min-h-screen bg-gray-50 dark:bg-black overflow-hidden text-gray-900 dark:text-white
w-[100vw] relative left-[50%] right-[50%] -ml-[50vw] -mr-[50vw]"
>

<main className="relative right-[50%] left-[50%] -mr-[50vw] -ml-[50vw] min-h-screen w-[100vw] overflow-hidden bg-gray-50 text-gray-900 dark:bg-black dark:text-white">
<HeroSection stats={stats} />
<TopicsSection />
<FeaturesSection />
<PricingSection sponsors={sponsors} />
<CommunitySection />

</main>
)
}
);
}
29 changes: 15 additions & 14 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import groq from 'groq';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import groq from 'groq';
import { getTranslations } from 'next-intl/server';

import { client } from '@/client';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';
import { Link } from '@/i18n/routing';
import { formatBlogDate } from '@/lib/blog/date';
import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground';

export const revalidate = 0;

Expand Down Expand Up @@ -189,7 +190,7 @@ function renderPortableTextBlock(block: any, index: number): React.ReactNode {
return (
<h6
key={block._key || `block-${index}`}
className="mt-6 mb-2 text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300"
className="mt-6 mb-2 text-sm font-semibold tracking-wide text-gray-700 uppercase dark:text-gray-300"
>
{children}
</h6>
Expand All @@ -209,7 +210,7 @@ function renderPortableTextBlock(block: any, index: number): React.ReactNode {
return (
<p
key={block._key || `block-${index}`}
className="mb-4 whitespace-pre-line text-base leading-relaxed text-gray-700 dark:text-gray-300"
className="mb-4 text-base leading-relaxed whitespace-pre-line text-gray-700 dark:text-gray-300"
>
{children}
</p>
Expand Down Expand Up @@ -283,7 +284,7 @@ function renderPortableText(
key={block._key || `image-${i}`}
src={block.url}
alt={postTitle || 'Post image'}
className="rounded-xl border border-gray-200 my-6"
className="my-6 rounded-xl border border-gray-200"
/>
);
i += 1;
Expand Down Expand Up @@ -482,7 +483,7 @@ export default async function PostDetails({
: null;

return (
<DynamicGridBackground className="bg-gray-50 transition-colors duration-300 dark:bg-transparent py-10">
<DynamicGridBackground className="bg-gray-50 py-10 transition-colors duration-300 dark:bg-transparent">
<main className="relative z-10 mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
{breadcrumbsJsonLd && (
<script
Expand Down Expand Up @@ -512,7 +513,7 @@ export default async function PostDetails({
{!isLast && item.href ? (
<Link
href={item.href}
className="transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
className="underline-offset-4 transition hover:text-[var(--accent-primary)] hover:underline"
>
{item.name}
</Link>
Expand All @@ -533,7 +534,7 @@ export default async function PostDetails({

<div className="mx-auto w-full max-w-3xl">
{categoryLabel && (
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
<div className="text-center text-sm font-medium text-gray-500 dark:text-gray-400">
<Link
href={categoryHref || '/blog'}
className="inline-flex items-center gap-1 text-[var(--accent-primary)] transition"
Expand All @@ -542,7 +543,7 @@ export default async function PostDetails({
</Link>
</div>
)}
<h1 className="mt-3 text-4xl font-bold text-gray-900 dark:text-gray-100 text-center">
<h1 className="mt-3 text-center text-4xl font-bold text-gray-900 dark:text-gray-100">
{post.title}
</h1>

Expand All @@ -569,7 +570,7 @@ export default async function PostDetails({
{(post.tags?.length || 0) > 0 && null}

{post.mainImage && (
<div className="relative w-full h-[520px] rounded-2xl overflow-hidden my-8">
<div className="relative my-8 h-[520px] w-full overflow-hidden rounded-2xl">
<Image
src={post.mainImage}
alt={post.title || 'Post image'}
Expand All @@ -596,7 +597,7 @@ export default async function PostDetails({
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{t('recommendedPosts')}
</h2>
<div className="mt-6 grid gap-6 auto-rows-fr sm:grid-cols-2 lg:grid-cols-3">
<div className="mt-6 grid auto-rows-fr gap-6 sm:grid-cols-2 lg:grid-cols-3">
{recommendedPosts.map(item => {
const itemCategory = item.categories?.[0];
const itemCategoryDisplay = itemCategory
Expand All @@ -619,18 +620,18 @@ export default async function PostDetails({
/>
</div>
)}
<h3 className="mt-4 text-lg font-semibold text-gray-900 transition group-hover:underline underline-offset-4 dark:text-gray-100">
<h3 className="mt-4 text-lg font-semibold text-gray-900 underline-offset-4 transition group-hover:underline dark:text-gray-100">
{item.title}
</h3>
{item.body && (
<p className="mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400 line-clamp-2">
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400">
{plainTextFromPortableText(item.body)}
</p>
)}
{(item.author?.name ||
itemCategoryDisplay ||
item.publishedAt) && (
<div className="mt-auto pt-3 flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<div className="mt-auto flex flex-wrap items-center gap-2 pt-3 text-sm text-gray-500 dark:text-gray-400">
{item.author?.image && (
<span className="relative h-5 w-5 overflow-hidden rounded-full">
<Image
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/[locale]/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import PostDetails from './PostDetails';
import { client } from '@/client';
import groq from 'groq';

import { client } from '@/client';

import PostDetails from './PostDetails';

export async function generateStaticParams() {
const slugs = await client.fetch<string[]>(
groq`*[_type == "post" && defined(slug.current)][].slug.current`
Expand Down
Loading
Loading