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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1026,3 +1026,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Fixed stale test helper import (`TEST_LEGAL_CONSENT`) by switching to runtime consent factory generation
- Fixed OAuth build/runtime fragility by lazy-loading provider env usage
- Fixed shop/runtime regressions around env delivery and SSR safety on Netlify develop

## [1.0.12] - 2026-04-23

### Added

- New category visual support:
- Added PHP, Laravel, C#, and .NET category styles and SVG icon mapping
- Introduced centralized `categoryRegistry` as a single source of truth for category metadata and style config

### Changed

- Q&A / Quiz category architecture:
- Refactored `categoryData` and `categoryTabStyles` generation to use shared registry-driven definitions
- Preserved existing style/data APIs (`categoryData`, `categoryTabStyles`, `getCategoryTabStyle`) for compatibility across pages
- About page social metric:
- Updated LinkedIn followers fallback target from `1.8k+` to `2k+`
- Normalized compact-number formatting so whole thousands render as `2k+` (without `.0`)
- Docs and project presentation:
- Refreshed README screenshots and dashboard section copy
- Updated README Blog section to reflect in-house admin workflow (not Sanity CMS)

### Fixed

- Shop cart SSR/runtime capability checks:
- Restored fail-closed cart provider resolution behavior to prevent SSR crashes when unrelated env is missing
- Migrated Stripe/Monobank cart capability env reads to runtime-safe server env access for Netlify
- Category typing robustness:
- Preserved literal slug/title types in `categoryRegistry` factory to avoid widening to `string`
- UI polish:
- Brightened Django category accent color for better readability on dark theme
106 changes: 19 additions & 87 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,10 @@
# DevLovers

A full-stack interview preparation platform for frontend, backend, and full-stack developers.

**Production:** [devlovers.net](https://devlovers.net)
**Develop:** [develop.devlovers.net](https://develop-devlovers.netlify.app)
[![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys)

## Overview

DevLovers helps developers prepare for technical interviews through:

- **Q&A Section** — Categorized interview questions (JavaScript, React, Node.js, etc.)
- **Quiz System** — Timed quizzes with anti-cheat, progress tracking, and leaderboards
- **Leaderboard** — Gamification with improvement-based scoring
- **Blog** — Technical articles managed via Sanity CMS
- **Shop** — E-commerce with Stripe payments and inventory management
- **About** — Landing page with platform stats, features overview, and community info

Supports three languages: Ukrainian, English, Polish.

## Tech Stack

| Category | Technologies |
| ---------- | ----------------------------------- |
| Framework | Next.js 16, React 19, TypeScript |
| Styling | Tailwind CSS v4, Framer Motion |
| Database | PostgreSQL (Neon), Drizzle ORM |
| Caching | Upstash Redis |
| CMS | Sanity Studio, GROQ |
| UI | Radix UI, Lucide Icons |
| Auth | JWT (jsonwebtoken), bcryptjs, OAuth |
| Payments | Stripe |
| Email | Nodemailer |
| i18n | next-intl |
| AI | Groq SDK |
| Testing | Vitest, React Testing Library |
| Media | Cloudinary |
| Deployment | Vercel |

## Project Structure

```
devlovers.net/
├── frontend/ # Next.js application
│ ├── app/
│ │ ├── [locale]/ # Localized pages (uk/en/pl)
│ │ └── api/ # API routes
│ ├── components/ # React components
│ ├── db/
│ │ ├── schema/ # Drizzle table definitions
│ │ └── queries/ # Reusable DB queries
│ ├── hooks/ # Custom React hooks
│ ├── i18n/ # Internationalization config
│ ├── lib/ # Utilities and business logic
│ └── messages/ # Translation files (JSON)
└── studio/ # Sanity CMS
└── schemaTypes/ # Content schemas (posts, authors)
```
# DevLovers Technical Interview Platform

> [!NOTE]
>
> ### Train smarter, interview stronger.
>
> DevLovers is a multi-language interview prep platform with curated Q&A, timed quizzes, leaderboard rankings, a developer blog, and a personal dashboard to track real learning progress.
Comment on lines +3 to +7
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 | 🟡 Minor

Fix the skipped heading level in the note.

### immediately after the H1 triggers markdownlint MD001. This can be bold text instead of a nested heading.

📝 Proposed fix
 > [!NOTE]
 >
-> ### Train smarter, interview stronger.
+> **Train smarter, interview stronger.**
 >
 > DevLovers is a multi-language interview prep platform with curated Q&A, timed quizzes, leaderboard rankings, a developer blog, and a personal dashboard to track real learning progress.
📝 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
> [!NOTE]
>
> ### Train smarter, interview stronger.
>
> DevLovers is a multi-language interview prep platform with curated Q&A, timed quizzes, leaderboard rankings, a developer blog, and a personal dashboard to track real learning progress.
> [!NOTE]
>
> **Train smarter, interview stronger.**
>
> DevLovers is a multi-language interview prep platform with curated Q&A, timed quizzes, leaderboard rankings, a developer blog, and a personal dashboard to track real learning progress.
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 3 - 7, Inside the [!NOTE] block in the README replace
the nested H3 "### Train smarter, interview stronger." with plain bold text
(e.g., **Train smarter, interview stronger.**) so the H1 is not immediately
followed by a lower-level heading; update the line containing "### Train
smarter, interview stronger." accordingly to remove the leading "###" and use
bold formatting instead.


## Main Features

Expand Down Expand Up @@ -103,7 +50,7 @@ devlovers.net/

### Blog

- Technical articles via Sanity CMS
- Technical articles via custom in-house admin panel
- Category and tag filtering
- Search with pagination
- Multi-language content (uk/en/pl)
Expand All @@ -126,33 +73,18 @@ devlovers.net/

![Shop](./assets/08-screencapture.png)

## Getting Started

```bash
# Install dependencies
cd frontend && npm install

# Set up environment variables
cp .env.example .env.local

# Run development server
npm run dev

# Database commands
npx drizzle-kit generate # Generate migrations
npx drizzle-kit push # Apply migrations
```

## Git Workflow

- `main` — production branch
- `develop` — development branch
- Feature branches: `prefix/feat/feature-name`
## Dashboard

Task tracking via [GitHub Projects](https://github.com/DevLoversTeam/devlovers.net/projects) board.
- Personal learning dashboard with progress insights
- Quiz history and performance overview
- Quick access to key account activity

## License
![Dashboard](./assets/09-screencapture.png)

**MIT**
> [!IMPORTANT]
> **Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)

**Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)
> [!TIP]
> **Production:** [devlovers.net](https://devlovers.net)
>
> **Develop:** [develop.devlovers.net](https://develop-devlovers.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys)
Comment on lines +84 to +90
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 | 🟡 Minor

Avoid adjacent blockquote lint failures.

The back-to-back admonitions and blank quoted line can trigger markdownlint MD028. Add an HTML separator between callouts and remove the blank blockquote line inside the TIP.

📝 Proposed fix
 > [!IMPORTANT]
 > **Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)
 
+<!-- -->
+
 > [!TIP]
 > **Production:** [devlovers.net](https://devlovers.net)
->
 > **Develop:** [develop.devlovers.net](https://develop-devlovers.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys)
📝 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
> [!IMPORTANT]
> **Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)
**Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)
> [!TIP]
> **Production:** [devlovers.net](https://devlovers.net)
>
> **Develop:** [develop.devlovers.net](https://develop-devlovers.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys)
> [!IMPORTANT]
> **Contact me:** [contact@devlovers.net](mailto:contact@devlovers.net)
<!-- -->
> [!TIP]
> **Production:** [devlovers.net](https://devlovers.net)
> **Develop:** [develop.devlovers.net](https://develop-devlovers.netlify.app) [![Netlify Status](https://api.netlify.com/api/v1/badges/0d21e84e-ea55-47f0-b841-c8eb91f2c9a0/deploy-status)](https://app.netlify.com/projects/develop-devlovers/deploys)
🧰 Tools
🪛 LanguageTool

[style] ~87-~87: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1575 characters long)
Context: ....net](mailto:contact@devlovers.net) > [!TIP] > Production: [devlovers.net](h...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.22.0)

[warning] 86-86: Blank line inside blockquote

(MD028, no-blanks-blockquote)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 84 - 90, Remove the empty blockquote line inside the
TIP admonition and insert an HTML separator between the two admonitions so
markdownlint MD028 is not triggered; specifically, edit the README.md section
containing the "[!IMPORTANT]" and "[!TIP]" blocks to delete the blank ">" line
inside the TIP block and add a separator (for example an HTML comment or <hr/>)
between the "[!IMPORTANT]" and "[!TIP]" callouts to prevent adjacent blockquote
lint failures.

Binary file modified assets/01-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/02-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/03-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/04-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/05-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/06-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/07-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/08-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/09-screencapture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 31 additions & 5 deletions frontend/app/[locale]/shop/cart/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
import { resolveStandardStorefrontProviderCapabilities } from '@/lib/shop/commercial-policy.server';
import { isMonobankEnabled } from '@/lib/env/monobank';
import { readServerEnv } from '@/lib/env/server-env';
import { isPaymentsEnabled as isStripePaymentsEnabled } from '@/lib/env/stripe';

function isFlagEnabled(value: string | undefined): boolean {
const normalized = (value ?? '').trim().toLowerCase();
return (
normalized === 'true' ||
normalized === '1' ||
normalized === 'yes' ||
normalized === 'on'
);
}

export function resolveStripeCheckoutEnabled(): boolean {
return resolveStandardStorefrontProviderCapabilities().stripeCheckoutEnabled;
try {
return isStripePaymentsEnabled({
requirePublishableKey: true,
});
} catch {
return false;
}
}

export function resolveMonobankCheckoutEnabled(): boolean {
return resolveStandardStorefrontProviderCapabilities().monobankCheckoutEnabled;
const paymentsEnabled = isFlagEnabled(readServerEnv('PAYMENTS_ENABLED'));
if (!paymentsEnabled) return false;

try {
return isMonobankEnabled();
} catch {
return false;
}
}

export function resolveMonobankGooglePayEnabled(): boolean {
return resolveStandardStorefrontProviderCapabilities()
.monobankGooglePayEnabled;
if (!resolveMonobankCheckoutEnabled()) return false;

return isFlagEnabled(readServerEnv('SHOP_MONOBANK_GPAY_ENABLED'));
}
Comment on lines 25 to 40
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 | 🟠 Major

Keep Monobank env reads inside the fail-closed boundary.

Line 26 and Line 39 can still throw before returning false, which can reintroduce the cart SSR/runtime crash path this change is trying to prevent.

🛡️ Proposed fail-closed adjustment
 export function resolveMonobankCheckoutEnabled(): boolean {
-  const paymentsEnabled = isFlagEnabled(readServerEnv('PAYMENTS_ENABLED'));
-  if (!paymentsEnabled) return false;
-
   try {
+    const paymentsEnabled = isFlagEnabled(readServerEnv('PAYMENTS_ENABLED'));
+    if (!paymentsEnabled) return false;
+
     return isMonobankEnabled();
   } catch {
     return false;
   }
 }
 
 export function resolveMonobankGooglePayEnabled(): boolean {
-  if (!resolveMonobankCheckoutEnabled()) return false;
-
-  return isFlagEnabled(readServerEnv('SHOP_MONOBANK_GPAY_ENABLED'));
+  try {
+    if (!resolveMonobankCheckoutEnabled()) return false;
+
+    return isFlagEnabled(readServerEnv('SHOP_MONOBANK_GPAY_ENABLED'));
+  } catch {
+    return false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/shop/cart/capabilities.ts around lines 25 - 40, Both
resolveMonobankCheckoutEnabled and resolveMonobankGooglePayEnabled can throw
when calling readServerEnv (and thus reintroduce SSR/runtime crashes); move any
readServerEnv and isFlagEnabled calls into the same fail-closed try/catch
boundary so exceptions are caught and the function returns false. Concretely, in
resolveMonobankCheckoutEnabled wrap the paymentsEnabled read and check
(readServerEnv('PAYMENTS_ENABLED') + isFlagEnabled) inside the try/catch that
also calls isMonobankEnabled and return false on any exception; do the same in
resolveMonobankGooglePayEnabled so the SHOP_MONOBANK_GPAY_ENABLED read is
performed inside a try/catch and returns false on error, referencing
resolveMonobankCheckoutEnabled, resolveMonobankGooglePayEnabled, isFlagEnabled,
readServerEnv, and isMonobankEnabled to locate the changes.

2 changes: 1 addition & 1 deletion frontend/components/about/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function HeroSection({ stats }: { stats?: PlatformStats }) {
questionsSolved: '850+',
githubStars: '120+',
activeUsers: '200+',
linkedinFollowers: '1.8k+',
linkedinFollowers: '2k+',
};

return (
Expand Down
33 changes: 5 additions & 28 deletions frontend/data/category.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { categoryRegistry } from './categoryRegistry';

const createCategory = (slug: string, title: string, displayOrder: number) => ({
slug,
displayOrder,
Expand All @@ -8,31 +10,6 @@ const createCategory = (slug: string, title: string, displayOrder: number) => ({
},
});

export const categoryData = [
createCategory('git', 'Git', 0),
createCategory('html', 'HTML', 1),
createCategory('css', 'CSS', 2),
createCategory('javascript', 'JavaScript', 3),
createCategory('typescript', 'TypeScript', 4),
createCategory('react', 'React', 5),
createCategory('next', 'Next.js', 6),
createCategory('vue', 'Vue.js', 7),
createCategory('angular', 'Angular', 8),
createCategory('node', 'Node.js', 9),
createCategory('sql', 'SQL', 10),
createCategory('postgresql', 'PostgreSQL', 11),
createCategory('mongodb', 'MongoDB', 12),
createCategory('python', 'Python', 13),
createCategory('django', 'Django', 14),
createCategory('docker', 'Docker', 15),
createCategory('kubernetes', 'Kubernetes', 16),
createCategory('aws', 'AWS', 17),
createCategory('azure', 'Azure', 18),
createCategory('devops', 'DevOps', 19),
createCategory('swift', 'Swift', 20),
createCategory('flutter', 'Flutter', 21),
createCategory('kotlin', 'Kotlin', 22),
createCategory('reactnative', 'React Native', 23),
createCategory('java', 'Java', 24),
createCategory('spring', 'Spring', 25),
];
export const categoryData = categoryRegistry.map(item =>
createCategory(item.slug, item.title, item.displayOrder)
);
Loading
Loading