Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.
19 changes: 11 additions & 8 deletions frontend/actions/quiz.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
'use server';

import { and,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
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
32 changes: 17 additions & 15 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import groq from 'groq';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import Image from 'next/image';

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

export const revalidate = 0;

Expand Down Expand Up @@ -87,33 +88,34 @@ export default async function BlogCategoryPage({
const featuredDate = formatBlogDate(featuredPost?.publishedAt);

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">
<nav className="mb-6" aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<li className="flex items-center gap-2">
<Link
href="/blog"
className="transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
className="underline-offset-4 transition hover:text-[var(--accent-primary)] hover:underline"
>
{tNav('blog')}
</Link>
<span>&gt;</span>
</li>
<li className="flex items-center gap-2">
<span className="text-[var(--accent-primary)]" aria-current="page">
<span
className="text-[var(--accent-primary)]"
aria-current="page"
>
{categoryDisplay}
</span>
</li>
</ol>
</nav>
<h1 className="text-4xl font-bold mb-4 text-left">
{categoryDisplay}
</h1>
<h1 className="mb-4 text-left text-4xl font-bold">{categoryDisplay}</h1>
{featuredPost?.mainImage && (
<section className="mt-10">
<article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black">
<div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]">
<div className="h-[320px] max-h-[65vh] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px]">
<Image
src={featuredPost.mainImage}
alt={featuredPost.title}
Expand All @@ -123,14 +125,14 @@ export default async function BlogCategoryPage({
priority={false}
/>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" />
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent sm:h-64 dark:from-black/90 dark:via-black/60" />
<div className="absolute inset-x-0 bottom-0 p-6 sm:p-8">
{featuredPost.categories?.[0] && (
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{featuredPost.categories[0]}
</div>
)}
<h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl">
<h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] sm:text-4xl dark:text-white dark:group-hover:text-[var(--accent-primary)]">
{featuredPost.title}
</h2>
<div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200">
Expand All @@ -157,7 +159,7 @@ export default async function BlogCategoryPage({
<FeaturedPostCtaButton
href={`/blog/${featuredPost.slug.current}`}
label={featuredPost.title || 'Read more'}
className="!absolute !bottom-6 !right-6 z-10 h-11 w-11 rounded-full bg-[var(--accent-primary)] text-white opacity-0 shadow-sm transition group-hover:opacity-100 focus-visible:opacity-100 group-focus-within:opacity-100"
className="!absolute !right-6 !bottom-6 z-10 h-11 w-11 rounded-full bg-[var(--accent-primary)] text-white opacity-0 shadow-sm transition group-focus-within:opacity-100 group-hover:opacity-100 focus-visible:opacity-100"
/>
</article>
</section>
Expand All @@ -166,7 +168,7 @@ export default async function BlogCategoryPage({
<BlogCategoryGrid posts={restPosts} />
</div>
{!posts.length && (
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>
<p className="mt-10 text-center text-gray-500">{t('noPosts')}</p>
)}
</main>
</DynamicGridBackground>
Expand Down
Loading
Loading