Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
725d234
feat(Blog):fix for clickable link in post details, fix for author det…
KomrakovaAnna Jan 24, 2026
6e8561d
feat(Blog):refactoring after removing author modal
KomrakovaAnna Jan 24, 2026
0334ea5
feat(Blog):mergin develop
KomrakovaAnna Jan 24, 2026
7bf3768
feat(Blog): fix unified date format
KomrakovaAnna Jan 24, 2026
d60fbe8
feat(Blog): Fix for click-outside-to-close search, recommended posts…
KomrakovaAnna Jan 25, 2026
c836b11
merging develop
KomrakovaAnna Jan 25, 2026
705f075
feat(Blog): selectedAuthorData fixed
KomrakovaAnna Jan 25, 2026
69a8190
feat(Blog): Added description for /blog/[slug] metadata, Added Schema…
KomrakovaAnna Jan 25, 2026
f628c8c
feat(Blog): fix hover social links, fixed duplication not found search
KomrakovaAnna Jan 25, 2026
a2fa0b6
Merge branch 'develop' into sanity
KomrakovaAnna Jan 25, 2026
42d1a62
feat(Blog): Added: breadcrumbs to the post details page and updated t…
KomrakovaAnna Jan 25, 2026
faab3bd
feat(Blog): Added: breadcrumbs to the post details page and updated t…
KomrakovaAnna Jan 25, 2026
102f427
merging develop
KomrakovaAnna Jan 25, 2026
91f05d8
feat(Blog): Added scroll on the main blog page on filtering by author…
KomrakovaAnna Jan 25, 2026
d73087a
Merge branch 'develop' into sanity
KomrakovaAnna Jan 25, 2026
9347874
feat(Blog): Changed image size on the post details page
KomrakovaAnna Jan 25, 2026
2591e67
Merge branch 'develop' into sanity
KomrakovaAnna Jan 25, 2026
a04e8bb
feat(Blog): added tests
KomrakovaAnna Jan 26, 2026
83d54a6
added package-lock
KomrakovaAnna Jan 26, 2026
f0a1419
feat(Blog): fix for big post on the post page, added tests
KomrakovaAnna Jan 26, 2026
fd582c2
feat(Blog): resolving comments
KomrakovaAnna Jan 26, 2026
f0144a3
Merge branch 'develop' into sanity
KomrakovaAnna Jan 26, 2026
556e400
feat(Blog): fixed hover for social links icins - dark theme
KomrakovaAnna Jan 26, 2026
7e54349
Merge branch 'develop' into sanity
KomrakovaAnna Jan 26, 2026
177a77e
feat(Blog): bringing the style on the blog page to a single site style
KomrakovaAnna Jan 27, 2026
ded4abf
Merge branch 'develop' into sanity
KomrakovaAnna Jan 27, 2026
9faffa5
feat(blog): aligning syles
KomrakovaAnna Jan 27, 2026
7299946
feat(blog): resolving comment from CodeRabbit
KomrakovaAnna Jan 27, 2026
1f293ac
feat(blog):fix comment for deployment
KomrakovaAnna Jan 27, 2026
6d7fffa
Merge branch 'develop' into sanity
KomrakovaAnna Feb 1, 2026
cafee1b
feat(Blog): adding pagination
KomrakovaAnna Feb 1, 2026
74bb624
feat(Blog): Addind Text formatting visibility
KomrakovaAnna Feb 1, 2026
5f907e4
feat(Blog):adding text formating fix
KomrakovaAnna Feb 1, 2026
a64eecd
feat(Blog):merging develop
KomrakovaAnna Feb 1, 2026
5a6ad1e
feat(blog):Paddings on mobile fixed
KomrakovaAnna Feb 2, 2026
dc2ab9c
feat(Blog): Refactoring
KomrakovaAnna Feb 3, 2026
8c081f4
feat(Blog): merging develop
KomrakovaAnna Feb 3, 2026
7e828be
feat(Blog):fix for eslint
KomrakovaAnna Feb 3, 2026
76083b6
feat(Blog): fixes of conflicts
KomrakovaAnna Feb 3, 2026
d7355b7
Merge branch 'develop' into sanity
KomrakovaAnna Feb 3, 2026
fc386d3
feat(blog): fix conflict
KomrakovaAnna Feb 3, 2026
e113491
feat(blog): resolved conflict
KomrakovaAnna Feb 3, 2026
d80e555
Merge branch 'develop' into sanity
KomrakovaAnna Feb 4, 2026
df07575
feat(Blog):fix conflict import
KomrakovaAnna Feb 4, 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
1 change: 1 addition & 0 deletions frontend/actions/quiz.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use server';

import { and, eq, inArray } from 'drizzle-orm';
import { eq, inArray } from 'drizzle-orm';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import { db } from '@/db';
Expand Down
8 changes: 5 additions & 3 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ function renderPortableTextSpans(
const text = child?.text || '';
if (!text) return null;
const marks = child?.marks || [];
const linkKey = marks.find(mark => linkMap.has(mark));

let node: React.ReactNode = marks.length === 0 ? linkifyText(text) : text;

Expand Down Expand Up @@ -280,11 +279,14 @@ function renderPortableText(

if (block?._type === 'image' && block?.url) {
nodes.push(
<img
<Image
key={block._key || `image-${i}`}
src={block.url}
alt={postTitle || 'Post image'}
className="my-6 rounded-xl border border-gray-200"
width={1200}
height={800}
sizes="100vw"
className="my-6 h-auto w-full rounded-xl border border-gray-200"
/>
);
i += 1;
Expand Down
29 changes: 12 additions & 17 deletions frontend/components/blog/BlogFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ export default function BlogFilters({
name: string;
norm: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const lastFiltersRef = useRef<{
author: string;
category: string;
Expand Down Expand Up @@ -327,11 +326,6 @@ export default function BlogFilters({
didClearSearchRef.current = true;
}, [pathname, router, searchParams, searchQuery]);

useEffect(() => {
if (currentPage === parsedPage) return;
setCurrentPage(parsedPage);
}, [currentPage, parsedPage]);

const resolvedAuthor = useMemo(() => {
const normParam = normalizeAuthor(authorParam);
if (!normParam) return selectedAuthor;
Expand Down Expand Up @@ -363,12 +357,11 @@ export default function BlogFilters({
prevFilters.query !== nextFilters.query;
if (!hasChanged) return;
lastFiltersRef.current = nextFilters;
if (currentPage !== 1) {
setCurrentPage(1);
if (parsedPage !== 1) {
updatePageParam(1);
}
}, [
currentPage,
parsedPage,
resolvedAuthor?.norm,
resolvedCategory?.norm,
searchQueryNormalized,
Expand Down Expand Up @@ -402,7 +395,7 @@ export default function BlogFilters({
}, [locale, resolvedAuthor?.name]);

useEffect(() => {
if (!resolvedAuthor) return;
if (!resolvedAuthor?.norm) return;
const frame = window.requestAnimationFrame(() => {
authorHeadingRef.current?.scrollIntoView({
behavior: 'smooth',
Expand Down Expand Up @@ -446,6 +439,7 @@ export default function BlogFilters({
1,
Math.ceil(filteredPosts.length / POSTS_PER_PAGE)
);
const currentPage = Math.min(parsedPage, totalPages);
const paginatedPosts = useMemo(() => {
const start = (currentPage - 1) * POSTS_PER_PAGE;
return filteredPosts.slice(start, start + POSTS_PER_PAGE);
Expand All @@ -462,10 +456,9 @@ export default function BlogFilters({
}, [selectedAuthorData]);

useEffect(() => {
if (currentPage <= totalPages) return;
setCurrentPage(totalPages);
if (parsedPage <= totalPages) return;
updatePageParam(totalPages);
}, [currentPage, totalPages, updatePageParam]);
}, [parsedPage, totalPages, updatePageParam]);

const scrollToCategoryFilters = useCallback(() => {
const target = categoryFiltersRef.current || postsGridRef.current;
Expand Down Expand Up @@ -494,11 +487,14 @@ export default function BlogFilters({
href={`/blog/${featuredPost.slug.current}`}
className="group block"
>
<div className="h-[300px] overflow-hidden rounded-3xl border-0 shadow-[0_12px_30px_rgba(0,0,0,0.12)] sm:h-[340px] md:h-full md:min-h-[400px] lg:min-h-[440px]">
<img
<div className="relative h-[300px] overflow-hidden rounded-3xl border-0 shadow-[0_12px_30px_rgba(0,0,0,0.12)] sm:h-[340px] md:h-full md:min-h-[400px] lg:min-h-[440px]">
<Image
src={featuredPost.mainImage}
alt={featuredPost.title}
className="block h-full w-full scale-[1.02] border-0 object-cover transition-transform duration-300 group-hover:scale-[1.05]"
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 60vw, 720px"
className="scale-[1.02] object-cover transition-transform duration-300 group-hover:scale-[1.05]"
priority
/>
</div>
</Link>
Expand Down Expand Up @@ -689,7 +685,6 @@ export default function BlogFilters({
totalPages={totalPages}
onPageChange={page => {
pendingScrollRef.current = true;
setCurrentPage(page);
updatePageParam(page);
}}
accentColor="var(--accent-primary)"
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/blog/BlogHeaderSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function BlogHeaderSearch() {
return () => {
active = false;
};
}, [open, items.length, isLoading]);
}, [open, items.length, isLoading, locale]);

useEffect(() => {
if (!open) return;
Expand Down
5 changes: 5 additions & 0 deletions frontend/components/quiz/QuizContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use client';
<<<<<<< HEAD

import { AlertTriangle, Ban, Clock, FileText } from 'lucide-react';
=======
import { Ban, Clock, FileText, TriangleAlert } from 'lucide-react';
>>>>>>> develop
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import { useRouter, useSearchParams } from 'next/navigation';
import { useLocale, useTranslations } from 'next-intl';
import {
Expand Down
3 changes: 2 additions & 1 deletion frontend/components/tests/blog/blog-card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { Author, Post } from '@/components/blog/BlogFilters';

vi.mock('next/image', () => ({
__esModule: true,
default: (props: any) => <img {...props} />,
// eslint-disable-next-line @next/next/no-img-element
default: (props: any) => <img alt={props.alt ?? ''} {...props} />,
}));

vi.mock('next/link', () => ({
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/tests/blog/blog-filters.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';

import type { Author, Post } from '@/components/blog/BlogFilters';
import BlogFilters from '@/components/blog/BlogFilters';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { BlogHeaderSearch } from '@/components/blog/BlogHeaderSearch';

Expand Down
162 changes: 162 additions & 0 deletions frontend/lib/tests/quiz/quiz-crypto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import {
createEncryptedAnswersBlob,
decryptAnswers,
encryptAnswers,
} from '@/lib/quiz/quiz-crypto';

import {
createCorrectAnswersMap,
createMockQuestions,
resetFactoryCounters,
} from '../factories/quiz/quiz';
import { cleanupQuizTestEnv, setupQuizTestEnv } from './setup';

describe('quiz-crypto', () => {
// Setup: set encryption key before each test
beforeEach(() => {
setupQuizTestEnv();
resetFactoryCounters();
});

// Cleanup: remove encryption key after each test
afterEach(() => {
cleanupQuizTestEnv();
});

describe('encryptAnswers', () => {
it('returns a base64 string', () => {
const answers = { 'q-1': 'a-1' };

const result = encryptAnswers(answers);

// Base64 pattern: letters, numbers, +, /, ends with optional =
expect(result).toMatch(/^[A-Za-z0-9+/]+=*$/);
});

it('returns different output for same input (random IV)', () => {
const answers = { 'q-1': 'a-1' };

const result1 = encryptAnswers(answers);
const result2 = encryptAnswers(answers);

// Each encryption uses random IV, so outputs differ
expect(result1).not.toBe(result2);
});

it('handles empty object', () => {
const result = encryptAnswers({});

expect(result).toBeDefined();
expect(typeof result).toBe('string');
});

it('handles multiple questions', () => {
const answers = {
'q-1': 'a-1',
'q-2': 'a-2',
'q-3': 'a-3',
};

const result = encryptAnswers(answers);

expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
});
});

describe('decryptAnswers', () => {
it('decrypts back to original data', () => {
const original = { 'q-1': 'a-1', 'q-2': 'a-2' };

const encrypted = encryptAnswers(original);
const decrypted = decryptAnswers(encrypted);

expect(decrypted).toEqual(original);
});

it('returns null for tampered data', () => {
const original = { 'q-1': 'a-1' };
const encrypted = encryptAnswers(original);

// Tamper: change last 5 characters
const tampered = encrypted.slice(0, -5) + 'XXXXX';

const result = decryptAnswers(tampered);

expect(result).toBeNull();
});

it('returns null for invalid base64', () => {
const result = decryptAnswers('not-valid-base64!!!');

expect(result).toBeNull();
});

it('returns null for empty string', () => {
const result = decryptAnswers('');

expect(result).toBeNull();
});

it('returns null for truncated data', () => {
const original = { 'q-1': 'a-1' };
const encrypted = encryptAnswers(original);

// Truncate: remove half of the data
const truncated = encrypted.slice(0, encrypted.length / 2);

const result = decryptAnswers(truncated);

expect(result).toBeNull();
});
});

describe('createEncryptedAnswersBlob', () => {
it('creates encrypted blob from questions', () => {
const questions = createMockQuestions(3);

const blob = createEncryptedAnswersBlob(questions);

expect(blob).toBeDefined();
expect(typeof blob).toBe('string');
expect(blob.length).toBeGreaterThan(0);
});

it('encrypted blob decrypts to correct answers map', () => {
const questions = createMockQuestions(3);
const expectedMap = createCorrectAnswersMap(questions);

const blob = createEncryptedAnswersBlob(questions);
const decrypted = decryptAnswers(blob);

expect(decrypted).toEqual(expectedMap);
});

it('handles questions with no correct answer', () => {
const questions = [
{
id: 'q-no-correct',
answers: [
{ id: 'a-1', isCorrect: false },
{ id: 'a-2', isCorrect: false },
],
},
];

const blob = createEncryptedAnswersBlob(questions);
const decrypted = decryptAnswers(blob);

// Question with no correct answer is not included in map
expect(decrypted).toEqual({});
});

it('handles empty questions array', () => {
const blob = createEncryptedAnswersBlob([]);
const decrypted = decryptAnswers(blob);

expect(decrypted).toEqual({});
});
});
});
5 changes: 5 additions & 0 deletions frontend/lib/tests/quiz/setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<<<<<<< HEAD
import { afterEach, beforeEach, vi } from 'vitest';

=======
>>>>>>> develop
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unresolved merge conflict blocks compilation.

The file contains Git conflict markers (<<<<<<<, =======, >>>>>>>) that must be resolved before merging. The HEAD side's import is required since vi.fn() is used throughout this file (lines 37, 38, 41, 44, 50).

🐛 Proposed fix: Resolve conflict by keeping the vitest import
-<<<<<<< HEAD
 import { afterEach, beforeEach, vi } from 'vitest';
-
-=======
->>>>>>> develop
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<<<<<<< HEAD
import { afterEach, beforeEach, vi } from 'vitest';
=======
>>>>>>> develop
import { afterEach, beforeEach, vi } from 'vitest';
🧰 Tools
🪛 Biome (2.3.13)

[error] 1-1: Expected a statement but instead found '<<<<<<< HEAD'.

Expected a statement here.

(parse)


[error] 3-5: Expected a statement but instead found '=======

develop'.

Expected a statement here.

(parse)

🤖 Prompt for AI Agents
In `@frontend/lib/tests/quiz/setup.ts` around lines 1 - 5, Resolve the Git
conflict markers in setup.ts by removing the <<<<<<<, =======, and >>>>>>> lines
and keeping the vitest import line "import { afterEach, beforeEach, vi } from
'vitest';" (these symbols are required because the file uses vi.fn(),
beforeEach, and afterEach); ensure only a single clean import statement remains
at the top so the existing vi.fn() calls compile.

/**
* 32-byte hex key for AES-256 (64 hex characters)
* Only used in tests — not a real secret
Expand Down
13 changes: 13 additions & 0 deletions frontend/lib/tests/quiz/verify-answer.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { NextRequest } from 'next/server';
<<<<<<< HEAD
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
=======
import { beforeEach, describe, expect, it, vi } from 'vitest';
>>>>>>> develop
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import { POST } from '@/app/api/quiz/verify-answer/route';

<<<<<<< HEAD
import {
createCorrectAnswersMap,
createMockQuestions,
resetFactoryCounters,
} from '../factories/quiz/quiz';
import { cleanupQuizTestEnv, setupQuizTestEnv } from './setup';
=======
// Mock the Redis module
vi.mock('@/lib/quiz/quiz-answers-redis', () => ({
getCorrectAnswer: vi.fn(),
Expand All @@ -11,6 +23,7 @@ vi.mock('@/lib/quiz/quiz-answers-redis', () => ({
import { getCorrectAnswer } from '@/lib/quiz/quiz-answers-redis';

const mockGetCorrectAnswer = vi.mocked(getCorrectAnswer);
>>>>>>> develop

function createVerifyRequest(body: unknown): NextRequest {
return new NextRequest('http://localhost/api/quiz/verify-answer', {
Expand Down
Loading