;
}) {
const cellClass =
- 'px-2 sm:px-6 py-3 sm:py-4 border-b border-slate-100 dark:border-white/5';
+ 'px-2 sm:px-6 py-3 sm:py-4 border-b border-gray-200/50 dark:border-white/5';
const leftBorderClass = 'border-l border-l-transparent';
const rightBorderClass = 'border-r border-r-transparent';
@@ -158,7 +158,7 @@ function TableRow({
'group transition-all duration-300',
isCurrentUser
? 'bg-[color-mix(in_srgb,var(--accent-primary),transparent_90%)] shadow-inner'
- : 'hover:bg-slate-50/60 dark:hover:bg-white/4'
+ : 'hover:bg-white/30 dark:hover:bg-white/5'
)}
>
@@ -174,7 +174,7 @@ function TableRow({
'relative h-8 w-8 shrink-0 overflow-hidden rounded-full border transition-all duration-300 sm:h-10 sm:w-10',
isCurrentUser
? 'border-(--accent-primary) shadow-[0_0_1px_var(--accent-primary)]'
- : 'border-slate-200 group-hover:border-(--accent-primary) dark:border-white/10'
+ : 'border-white/20 group-hover:border-(--accent-primary) dark:border-white/10'
)}
>
{user.username}
@@ -248,7 +248,7 @@ function TableRow({
'inline-block font-mono font-bold transition-all',
isCurrentUser
? 'scale-110 text-sm text-(--accent-primary) drop-shadow-sm sm:text-lg'
- : 'text-sm text-slate-700 group-hover:scale-105 sm:text-base dark:text-slate-300'
+ : 'text-sm text-gray-800 group-hover:scale-105 sm:text-base dark:text-gray-200'
)}
>
{user.points.toLocaleString()}
@@ -261,8 +261,8 @@ function TableRow({
function RankBadge({ rank }: { rank: number }) {
if (rank === 1) {
return (
-
-
+
+
1
@@ -272,7 +272,7 @@ function RankBadge({ rank }: { rank: number }) {
}
if (rank === 2) {
return (
-
+
2
@@ -282,8 +282,8 @@ function RankBadge({ rank }: { rank: number }) {
}
if (rank === 3) {
return (
-
-
+
+
3
@@ -291,7 +291,7 @@ function RankBadge({ rank }: { rank: number }) {
);
}
return (
-
+
{rank}
);
diff --git a/frontend/components/quiz/QuizCard.tsx b/frontend/components/quiz/QuizCard.tsx
index 610410c1..c957469e 100644
--- a/frontend/components/quiz/QuizCard.tsx
+++ b/frontend/components/quiz/QuizCard.tsx
@@ -112,15 +112,17 @@ export function QuizCard({ quiz, userProgress }: QuizCardProps) {
{userProgress && (
-
+
- {t('best')} {userProgress.bestScore}/{userProgress.totalQuestions}
+ {t('best')} {userProgress.bestScore}/{userProgress.totalQuestions}
-
- {userProgress.attemptsCount}{' '}
+ ·
+
+ {userProgress.attemptsCount}{' '}
{userProgress.attemptsCount === 1 ? t('attempt') : t('attempts')}
-
+ ·
+
{percentage}%
diff --git a/frontend/components/quiz/QuizResult.tsx b/frontend/components/quiz/QuizResult.tsx
index 5caf8581..8aa4ef7f 100644
--- a/frontend/components/quiz/QuizResult.tsx
+++ b/frontend/components/quiz/QuizResult.tsx
@@ -100,13 +100,30 @@ export function QuizResult({
{motivation.icon}
{!isIncomplete && (
<>
-
-
- {score} / {total}
-
-
- {percentage.toFixed(0)}% {t('correctAnswers')}
-
+
+
+
+ {t('accuracy', { fallback: 'Accuracy' })}
+
+
+ {percentage.toFixed(0)}%
+
+
+ {score} / {total} {t('correctAnswers')}
+
+
+
+
+
+ {t('integrity', { fallback: 'Integrity' })}
+
+
+ {Math.max(0, 100 - (violationsCount || 0) * 10)}%
+
+
+ {violationsCount || 0} {t('violationsLabel', { fallback: 'Violations' })}
+
+
diff --git a/frontend/db/queries/users.ts b/frontend/db/queries/users.ts
index d9370a1b..b447404f 100644
--- a/frontend/db/queries/users.ts
+++ b/frontend/db/queries/users.ts
@@ -16,6 +16,8 @@ export const getUserProfile = cache(async (userId: string) => {
email: true,
image: true,
role: true,
+ provider: true,
+ providerId: true,
createdAt: true,
},
with: {
@@ -40,3 +42,31 @@ export const getUserProfile = cache(async (userId: string) => {
points: Number(pointsResult[0]?.total) || 0,
};
});
+
+export const getUserGlobalRank = cache(async (userId: string) => {
+ // Get all users' total points by grouping transactions
+ const rankQuery = sql`
+ WITH user_scores AS (
+ SELECT user_id, COALESCE(SUM(points), 0) as total_points
+ FROM point_transactions
+ GROUP BY user_id
+ ),
+ ranked_users AS (
+ SELECT user_id, total_points,
+ RANK() OVER (ORDER BY total_points DESC) as rank
+ FROM user_scores
+ )
+ SELECT rank
+ FROM ranked_users
+ WHERE user_id = ${userId}
+ `;
+
+ const result = await db.execute(rankQuery);
+ const rankRow = (result as { rows: any[] }).rows[0];
+
+ if (!rankRow || !rankRow.rank) {
+ return null;
+ }
+
+ return Number(rankRow.rank);
+});
diff --git a/frontend/lib/about/github-sponsors.ts b/frontend/lib/about/github-sponsors.ts
index 18d6af87..c5781328 100644
--- a/frontend/lib/about/github-sponsors.ts
+++ b/frontend/lib/about/github-sponsors.ts
@@ -21,7 +21,7 @@ function getTierDetails(amount: number): {
return { name: '☕ Coffee Support', color: 'bronze' };
}
-export async function getSponsors(): Promise {
+async function fetchSponsors(activeOnly: boolean): Promise {
const token = process.env.GITHUB_SPONSORS_TOKEN;
if (!token) {
@@ -32,7 +32,7 @@ export async function getSponsors(): Promise {
const query = `
query {
organization(login: "DevLoversTeam") {
- sponsorshipsAsMaintainer(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, includePrivate: false) {
+ sponsorshipsAsMaintainer(first: 100, orderBy: {field: CREATED_AT, direction: DESC}, includePrivate: false, activeOnly: ${activeOnly}) {
nodes {
tier { monthlyPriceInDollars }
sponsorEntity {
@@ -53,7 +53,7 @@ export async function getSponsors(): Promise {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
- cache: 'no-store', // Завжди свіжі дані
+ cache: 'no-store',
});
const json = await res.json();
@@ -67,7 +67,7 @@ export async function getSponsors(): Promise {
json.data?.organization?.sponsorshipsAsMaintainer?.nodes || [];
console.log(
- `✅ GitHub: Found ${rawNodes.length} sponsors for Organization`
+ `✅ GitHub: Found ${rawNodes.length} sponsors (activeOnly=${activeOnly})`
);
const sponsors: Sponsor[] = rawNodes
@@ -95,3 +95,16 @@ export async function getSponsors(): Promise {
return [];
}
}
+
+/** Active sponsors only — used for public-facing sponsor displays. */
+export async function getSponsors(): Promise {
+ return fetchSponsors(true);
+}
+
+/**
+ * All sponsors (active + past/cancelled) — used for achievement checks.
+ * A user who donated once and later cancelled still earns the Supporter badge.
+ */
+export async function getAllSponsors(): Promise {
+ return fetchSponsors(false);
+}
diff --git a/frontend/lib/about/stats.ts b/frontend/lib/about/stats.ts
index 2b0ea081..ec988638 100644
--- a/frontend/lib/about/stats.ts
+++ b/frontend/lib/about/stats.ts
@@ -43,7 +43,7 @@ export const getPlatformStats = unstable_cache(
const linkedinCount = process.env.LINKEDIN_FOLLOWER_COUNT
? parseInt(process.env.LINKEDIN_FOLLOWER_COUNT)
- : 1500;
+ : 1600;
let totalUsers = 243;
let solvedTests = 1890;
diff --git a/frontend/lib/achievements.ts b/frontend/lib/achievements.ts
new file mode 100644
index 00000000..2defd07a
--- /dev/null
+++ b/frontend/lib/achievements.ts
@@ -0,0 +1,127 @@
+export type AchievementIconName =
+ | 'Fire'
+ | 'Target'
+ | 'Lightning'
+ | 'Brain'
+ | 'Diamond'
+ | 'Star'
+ | 'Heart'
+ | 'Trophy'
+ | 'Rocket'
+ | 'Crown'
+ | 'Code'
+ | 'Infinity'
+ // batch 2
+ | 'GithubLogo'
+ | 'Medal'
+ | 'Seal'
+ | 'Moon'
+ | 'Shield'
+ | 'Waves';
+
+export interface Achievement {
+ id: string;
+ icon: AchievementIconName;
+ /** gradient colors for the badge circle [from, to] */
+ gradient: [string, string];
+ /** glow color (css color) */
+ glow: string;
+}
+
+export interface EarnedAchievement extends Achievement {
+ earned: boolean;
+ /** 0–100, only meaningful when earned === false */
+ progress?: number;
+ /** Date string when the achievement was earned */
+ earnedAt?: string;
+}
+
+export const ACHIEVEMENTS: Achievement[] = [
+ // ── Group 1: Quiz progression (attempts & points) ─────────────────
+ { id: 'first_blood', icon: 'Fire', gradient: ['#f97316', '#ef4444'], glow: 'rgba(249,115,22,0.55)' },
+ { id: 'on_a_roll', icon: 'Lightning', gradient: ['#eab308', '#f59e0b'], glow: 'rgba(234,179,8,0.55)' },
+ { id: 'rocket_start', icon: 'Rocket', gradient: ['#10b981', '#059669'], glow: 'rgba(16,185,129,0.55)' },
+ { id: 'big_brain', icon: 'Brain', gradient: ['#06b6d4', '#3b82f6'], glow: 'rgba(6,182,212,0.55)' },
+ { id: 'centurion', icon: 'Shield', gradient: ['#10b981', '#14b8a6'], glow: 'rgba(16,185,129,0.6)' },
+ { id: 'endless', icon: 'Infinity', gradient: ['#14b8a6', '#0ea5e9'], glow: 'rgba(20,184,166,0.55)' },
+
+ // ── Group 2: Skill / accuracy ──────────────────────────────────────
+ { id: 'sharpshooter', icon: 'Target', gradient: ['#6366f1', '#8b5cf6'], glow: 'rgba(99,102,241,0.55)' },
+ { id: 'perfectionist', icon: 'Star', gradient: ['#f59e0b', '#fbbf24'], glow: 'rgba(251,191,36,0.55)' },
+ { id: 'diamond_mind', icon: 'Diamond', gradient: ['#67e8f9', '#a78bfa'], glow: 'rgba(167,139,250,0.55)'},
+ { id: 'deep_diver', icon: 'Waves', gradient: ['#0ea5e9', '#6366f1'], glow: 'rgba(14,165,233,0.6)' },
+ { id: 'code_wizard', icon: 'Code', gradient: ['#8b5cf6', '#6366f1'], glow: 'rgba(139,92,246,0.55)' },
+ { id: 'legend', icon: 'Trophy', gradient: ['#d97706', '#b45309'], glow: 'rgba(217,119,6,0.55)' },
+
+ // ── Group 3: Social / special / sponsor ───────────────────────────
+ { id: 'royalty', icon: 'Crown', gradient: ['#f59e0b', '#dc2626'], glow: 'rgba(245,158,11,0.55)' },
+ { id: 'night_owl', icon: 'Moon', gradient: ['#6366f1', '#8b5cf6'], glow: 'rgba(99,102,241,0.6)' },
+ { id: 'star_gazer', icon: 'GithubLogo',gradient: ['#fbbf24', '#f59e0b'], glow: 'rgba(251,191,36,0.7)' },
+ { id: 'supporter', icon: 'Heart', gradient: ['#ec4899', '#f43f5e'], glow: 'rgba(236,72,153,0.55)' },
+ { id: 'silver_patron', icon: 'Medal', gradient: ['#94a3b8', '#e2e8f0'], glow: 'rgba(148,163,184,0.6)' },
+ { id: 'golden_patron', icon: 'Seal', gradient: ['#f59e0b', '#b45309'], glow: 'rgba(245,158,11,0.7)' },
+];
+
+export interface UserStats {
+ totalAttempts: number;
+ averageScore: number;
+ perfectScores: number;
+ highScores: number;
+ isSponsor: boolean;
+ uniqueQuizzes: number;
+ totalPoints: number;
+ topLeaderboard: boolean;
+ hasStarredRepo: boolean;
+ sponsorCount: number;
+ hasNightOwl: boolean;
+}
+
+export function computeAchievements(stats: UserStats): EarnedAchievement[] {
+
+
+ return ACHIEVEMENTS.map((a) => {
+ let earned = false;
+ let progress = 0;
+
+ switch (a.id) {
+ case 'first_blood': earned = stats.totalAttempts >= 1; progress = earned ? 100 : 0; break;
+ case 'sharpshooter': earned = stats.perfectScores >= 1; progress = earned ? 100 : 0; break;
+ case 'royalty': earned = stats.topLeaderboard; progress = earned ? 100 : 0; break;
+ case 'supporter': earned = stats.isSponsor; progress = earned ? 100 : 0; break;
+ case 'star_gazer': earned = stats.hasStarredRepo; progress = earned ? 100 : 0; break;
+ case 'night_owl': earned = stats.hasNightOwl; progress = earned ? 100 : 0; break;
+
+ case 'on_a_roll': earned = stats.totalAttempts >= 3; progress = Math.min((stats.totalAttempts / 3) * 100, 100); break;
+ case 'big_brain': earned = stats.totalAttempts >= 10; progress = Math.min((stats.totalAttempts / 10) * 100, 100); break;
+ case 'rocket_start': earned = stats.totalAttempts >= 5; progress = Math.min((stats.totalAttempts / 5) * 100, 100); break;
+ case 'diamond_mind': earned = stats.highScores >= 5; progress = Math.min((stats.highScores / 5) * 100, 100); break;
+ case 'perfectionist': earned = stats.perfectScores >= 3; progress = Math.min((stats.perfectScores / 3) * 100, 100); break;
+ case 'legend': earned = stats.uniqueQuizzes >= 10; progress = Math.min((stats.uniqueQuizzes / 10) * 100, 100); break;
+ case 'code_wizard': earned = stats.uniqueQuizzes >= 5; progress = Math.min((stats.uniqueQuizzes / 5) * 100, 100); break;
+ case 'endless': earned = stats.totalPoints >= 1000; progress = Math.min((stats.totalPoints / 1000) * 100, 100); break;
+ case 'centurion': earned = stats.totalPoints >= 100; progress = Math.min((stats.totalPoints / 100) * 100, 100); break;
+ case 'golden_patron': earned = stats.sponsorCount >= 3; progress = Math.min((stats.sponsorCount / 3) * 100, 100); break;
+
+ case 'silver_patron':
+ earned = stats.sponsorCount >= 2;
+ progress = stats.sponsorCount >= 2 ? 100 : stats.sponsorCount >= 1 ? 50 : 0;
+ break;
+
+ case 'deep_diver':
+ earned = stats.totalAttempts >= 10 && stats.averageScore >= 80;
+ progress = stats.totalAttempts < 10
+ ? Math.min((stats.totalAttempts / 10) * 100, 100)
+ : Math.min((stats.averageScore / 80) * 100, 100);
+ break;
+
+ default: earned = false; progress = 0;
+ }
+
+ return {
+ ...a,
+ earned,
+ progress,
+ earnedAt: undefined,
+ };
+ });
+}
diff --git a/frontend/lib/github-stars.ts b/frontend/lib/github-stars.ts
new file mode 100644
index 00000000..1d799c31
--- /dev/null
+++ b/frontend/lib/github-stars.ts
@@ -0,0 +1,86 @@
+import 'server-only';
+
+const REPO_OWNER = 'DevLoversTeam';
+const REPO_NAME = 'devlovers.net';
+const MAX_PAGES = 10; // caps at 1000 stargazers
+
+function getToken() {
+ return process.env.GITHUB_SPONSORS_TOKEN;
+}
+
+function makeHeaders(): Record {
+ return {
+ Accept: 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28',
+ Authorization: `Bearer ${getToken()}`,
+ };
+}
+
+/**
+ * Resolves a GitHub login (username) from a numeric provider ID.
+ * Used when the user signed in via GitHub OAuth — the DB stores the numeric
+ * `providerId`, but the stargazers API works with logins.
+ */
+export async function resolveGitHubLogin(providerId: string): Promise {
+ const token = getToken();
+ if (!token || !providerId) return null;
+
+ try {
+ const res = await fetch(`https://api.github.com/user/${providerId}`, {
+ headers: makeHeaders(),
+ next: { revalidate: 3600 },
+ });
+ if (!res.ok) return null;
+ const data: { login?: string } = await res.json();
+ return data.login ?? null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Checks whether a given GitHub login has starred the DevLovers repo.
+ * Uses the GITHUB_SPONSORS_TOKEN (server-side org PAT) — no user token needed.
+ * Paginates through stargazers up to MAX_PAGES × 100 entries.
+ */
+export async function checkHasStarredRepo(
+ githubLogin: string,
+): Promise {
+ const token = getToken();
+ if (!token || !githubLogin) return false;
+
+ const loginLower = githubLogin.toLowerCase();
+
+ for (let page = 1; page <= MAX_PAGES; page++) {
+ const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/stargazers?per_page=100&page=${page}`;
+
+ try {
+ const res = await fetch(url, {
+ headers: makeHeaders(),
+ next: { revalidate: 3600 }, // cache 1 hour per page
+ });
+
+ if (!res.ok) {
+ console.warn(`⚠️ GitHub stargazers API error: ${res.status}`);
+ return false;
+ }
+
+ const stargazers: { login: string }[] = await res.json();
+
+ // Empty page = we've exhausted all stargazers
+ if (stargazers.length === 0) return false;
+
+ if (stargazers.some((s) => s.login.toLowerCase() === loginLower)) {
+ return true;
+ }
+
+ // Last page (less than 100 results) — user not found
+ if (stargazers.length < 100) return false;
+ } catch (err) {
+ console.error('❌ Failed to check GitHub stargazers:', err);
+ return false;
+ }
+ }
+
+ return false;
+}
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index a7d5a944..f7a35303 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -204,6 +204,9 @@
"hurryUp": "Hurry up!"
},
"result": {
+ "accuracy": "Accuracy",
+ "integrity": "Integrity",
+ "violationsLabel": "Violations",
"correctAnswers": "correct answers",
"timeUp": {
"title": "Time's Up",
@@ -1081,24 +1084,52 @@
"metaDescription": "Track your progress and quiz performance",
"title": "Dashboard",
"subtitle": "Welcome back to your training ground",
- "supportLink": "Support & Feedback",
+ "supportLink": "Feedback",
"profile": {
"defaultName": "Developer",
"defaultRole": "user",
"sponsor": "Sponsor",
"becomeSponsor": "Become a Sponsor",
+ "supportAgain": "Show More Love",
"sponsorThanks": "Thank you for your support!",
"sponsorMore": "Support even more",
- "totalPoints": "Total Points",
- "joined": "Joined"
+ "totalPoints": "Points",
+ "joined": "Joined",
+ "globalRank": "Global Rank",
+ "dayStreak": "Day Streak",
+ "daysStreak": "Days Streak",
+ "settings": "Settings",
+ "editProfile": "Edit Profile",
+ "changeName": "Change Name",
+ "saveChanges": "Save changes",
+ "changePassword": "Change Password",
+ "currentPassword": "Current password",
+ "newPassword": "New password",
+ "saving": "Saving..."
},
"stats": {
"title": "Quiz Statistics",
- "noActivity": "Ready to level up? Challenge yourself with a new quiz",
- "startQuiz": "Start a Quiz",
+ "scoreDistribution": "Score Distribution",
+ "scoreDistributionSubtext": "Based on your recent attempts",
+ "activityHeatmap": "Activity Heatmap",
"attempts": "Attempts",
- "avgScore": "Avg Score",
- "continueLearning": "Continue Learning"
+ "avgScore": "Average Score",
+ "mastered": "Mastered",
+ "review": "Review",
+ "study": "Study",
+ "totalAttempts": "Quizzes",
+ "noActivity": "You haven't taken any quizzes yet. Start learning and track your progress here!",
+ "startQuiz": "Start your first quiz",
+ "continueLearning": "Continue Learning",
+ "less": "Less",
+ "more": "More",
+ "attemptsInPeriod": "{count} attempts in the period",
+ "last3Months": "Last 3 Months",
+ "last5Months": "Last 5 Months",
+ "last6Months": "Last 6 Months",
+ "lastYear": "This Year",
+ "totalActiveDays": "Active Days",
+ "mostActiveMonth": "Active Month"
},
"quizSaved": {
"title": "Quiz result saved!",
@@ -1114,6 +1145,7 @@
"noAttempts": "You haven't taken any quizzes yet",
"startQuiz": "Try one",
"score": "Score",
+ "accuracy": "Accuracy",
"integrity": "Integrity",
"points": "Points",
"scoreHint": "Number of correct answers out of total questions",
@@ -1167,9 +1199,112 @@
"messagePlaceholder": "Tell us what you think...",
"submit": "Send Feedback",
"submitting": "Sending...",
+ "attachFile": "Attach File",
"success": "Thank you! Your feedback has been sent.",
"error": "Something went wrong. Please try again.",
"requiredField": "Please fill out this field."
+ },
+ "achievements": {
+ "title": "Achievements",
+ "subtitle": "{earned} of {total} earned",
+ "ui": {
+ "expand": "View all",
+ "collapse": "Collapse",
+ "clickInfo": "Click for info",
+ "clickBack": "Click to flip back"
+ },
+ "badges": {
+ "first_blood": {
+ "name": "First Blood",
+ "desc": "Completed your first quiz!",
+ "hint": "Complete 1 quiz to unlock"
+ },
+ "sharpshooter": {
+ "name": "Sharpshooter",
+ "desc": "Scored 100% on a quiz!",
+ "hint": "Score 100% on any quiz"
+ },
+ "on_a_roll": {
+ "name": "On a Roll",
+ "desc": "Completed 3 quizzes!",
+ "hint": "Complete 3 quizzes to unlock"
+ },
+ "big_brain": {
+ "name": "Big Brain",
+ "desc": "Completed 10 quizzes!",
+ "hint": "Complete 10 quizzes to unlock"
+ },
+ "diamond_mind": {
+ "name": "Diamond Mind",
+ "desc": "Scored 90%+ on 5 quizzes!",
+ "hint": "Score 90%+ on 5 quizzes"
+ },
+ "perfectionist": {
+ "name": "Perfectionist",
+ "desc": "3 perfect scores achieved!",
+ "hint": "Score 100% on 3 quizzes"
+ },
+ "supporter": {
+ "name": "Supporter",
+ "desc": "Thank you for sponsoring!",
+ "hint": "Become a GitHub sponsor"
+ },
+ "legend": {
+ "name": "Legend",
+ "desc": "Completed 10 unique quizzes!",
+ "hint": "Complete 10 different quizzes"
+ },
+ "rocket_start": {
+ "name": "Rocket Start",
+ "desc": "Completed 5 quizzes!",
+ "hint": "Complete 5 quizzes to unlock"
+ },
+ "royalty": {
+ "name": "Royalty",
+ "desc": "Reached the top of the leaderboard!",
+ "hint": "Reach the top leaderboard position"
+ },
+ "code_wizard": {
+ "name": "Code Wizard",
+ "desc": "Mastered 5 different quiz topics!",
+ "hint": "Complete 5 unique quiz topics"
+ },
+ "endless": {
+ "name": "Endless",
+ "desc": "Earned 1000+ total points!",
+ "hint": "Earn 1000 total points"
+ },
+ "star_gazer": {
+ "name": "Star Gazer",
+ "desc": "You starred the DevLovers repo on GitHub!",
+ "hint": "Star our GitHub repository to unlock"
+ },
+ "silver_patron": {
+ "name": "Silver Patron",
+ "desc": "You've sponsored us twice — thank you!",
+ "hint": "Sponsor DevLovers a 2nd time to unlock"
+ },
+ "golden_patron": {
+ "name": "Golden Patron",
+ "desc": "You've sponsored us 3+ times — legendary!",
+ "hint": "Sponsor DevLovers a 3rd time to unlock"
+ },
+ "night_owl": {
+ "name": "Night Owl",
+ "desc": "Completed a quiz after midnight!",
+ "hint": "Complete a quiz after midnight"
+ },
+ "centurion": {
+ "name": "Centurion",
+ "desc": "Reached 100 total points!",
+ "hint": "Earn 100 total points"
+ },
+ "deep_diver": {
+ "name": "Deep Diver",
+ "desc": "Averaged 80%+ across 10+ quizzes!",
+ "hint": "Average 80%+ across 10 or more attempts"
+ }
+ }
}
},
"aiHelper": {
diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json
index 96fc1a5d..6d72c0a9 100644
--- a/frontend/messages/pl.json
+++ b/frontend/messages/pl.json
@@ -204,6 +204,9 @@
"hurryUp": "Pośpiesz się!"
},
"result": {
+ "accuracy": "Skuteczność",
+ "integrity": "Uczciwość",
+ "violationsLabel": "Naruszenia",
"correctAnswers": "poprawnych odpowiedzi",
"timeUp": {
"title": "Czas Minął",
@@ -1081,24 +1084,53 @@
"metaDescription": "Śledź swój postęp i wyniki quizów",
"title": "Panel użytkownika",
"subtitle": "Witaj z powrotem na swoim placu treningowym",
- "supportLink": "Wsparcie i opinie",
+ "supportLink": "Opinie",
"profile": {
- "defaultName": "Programista",
- "defaultRole": "użytkownik",
- "sponsor": "Sponsor",
+ "title": "Profil",
+ "subtitle": "Zarządzaj swoim profilem publicznym i preferencjami",
"becomeSponsor": "Zostań sponsorem",
+ "supportAgain": "Okaż więcej wsparcia",
+ "points": "Pkt",
+ "quizzesTaken": "Quizy",
+ "joined": "Dołączył",
+ "globalRank": "Miejsce",
+ "dayStreak": "Dzień z rzędu",
+ "daysStreak": "Dni z rzędu",
"sponsorThanks": "Dziękujemy za wsparcie!",
"sponsorMore": "Wesprzyj jeszcze więcej",
"totalPoints": "Łączna liczba punktów",
- "joined": "Dołączył"
+ "settings": "Ustawienia",
+ "editProfile": "Edytuj profil",
+ "changeName": "Zmień imię",
+ "saveChanges": "Zapisz zmiany",
+ "changePassword": "Zmień hasło",
+ "currentPassword": "Aktualne hasło",
+ "newPassword": "Nowe hasło",
+ "saving": "Zapisywanie..."
},
"stats": {
"title": "Statystyki quizów",
- "noActivity": "Gotowy na rozwój? Sprawdź się w nowym quizie",
- "startQuiz": "Rozpocznij quiz",
+ "scoreDistribution": "Rozkład wyników",
+ "scoreDistributionSubtext": "Na podstawie ostatnich podejść",
+ "activityHeatmap": "Wykres aktywności",
"attempts": "Próby",
"avgScore": "Średni wynik",
- "continueLearning": "Kontynuuj naukę"
+ "mastered": "Opanowane",
+ "review": "Wymaga powtórki",
+ "study": "Nauka",
+ "totalAttempts": "Wszystkie próby",
+ "noActivity": "Nie rozwiązałeś jeszcze żadnego quizu. Rozpocznij naukę i śledź swoje postępy tutaj!",
+ "startQuiz": "Rozpocznij pierwszy quiz",
+ "continueLearning": "Kontynuuj naukę",
+ "less": "Mniej",
+ "more": "Więcej",
+ "attemptsInPeriod": "{count} prób w tym okresie",
+ "last3Months": "Ostatnie 3 miesiące",
+ "last5Months": "Ostatnie 5 miesięcy",
+ "last6Months": "Ostatnie 6 miesięcy",
+ "lastYear": "Ten rok",
+ "totalActiveDays": "Aktywne dni",
+ "mostActiveMonth": "Aktywny miesiąc"
},
"quizSaved": {
"title": "Wynik quizu zapisany!",
@@ -1114,6 +1146,7 @@
"noAttempts": "Nie przeszedłeś jeszcze żadnego quizu",
"startQuiz": "Spróbuj",
"score": "Wynik",
+ "accuracy": "Skuteczność",
"integrity": "Czystość",
"points": "Punkty",
"scoreHint": "Liczba poprawnych odpowiedzi z ogólnej liczby",
@@ -1167,9 +1200,112 @@
"messagePlaceholder": "Powiedz nam, co myślisz...",
"submit": "Wyślij opinię",
"submitting": "Wysyłanie...",
+ "attachFile": "Dołącz plik",
"success": "Dziękujemy! Twoja opinia została wysłana.",
"error": "Coś poszło nie tak. Spróbuj ponownie.",
"requiredField": "Proszę wypełnić to pole."
+ },
+ "achievements": {
+ "title": "Osiągnięcia",
+ "subtitle": "{earned} z {total} odblokowanych",
+ "ui": {
+ "expand": "Zobacz wszystkie",
+ "collapse": "Zwiń",
+ "clickInfo": "Kliknij po info",
+ "clickBack": "Kliknij, aby wrócić"
+ },
+ "badges": {
+ "first_blood": {
+ "name": "Pierwszy krok",
+ "desc": "Ukończyłeś pierwszy quiz!",
+ "hint": "Ukończ 1 quiz"
+ },
+ "sharpshooter": {
+ "name": "Snajper",
+ "desc": "100% w quizie!",
+ "hint": "Zdobądź 100% w dowolnym quizie"
+ },
+ "on_a_roll": {
+ "name": "W biegu",
+ "desc": "Ukończyłeś 3 quizy!",
+ "hint": "Ukończ 3 quizy"
+ },
+ "big_brain": {
+ "name": "Wielki mózg",
+ "desc": "Ukończyłeś 10 quizów!",
+ "hint": "Ukończ 10 quizów"
+ },
+ "diamond_mind": {
+ "name": "Diamentowy umysł",
+ "desc": "90%+ w 5 quizach!",
+ "hint": "Zdobądź 90%+ w 5 quizach"
+ },
+ "perfectionist": {
+ "name": "Perfekcjonista",
+ "desc": "3 idealne wyniki!",
+ "hint": "Zdobądź 100% w 3 quizach"
+ },
+ "supporter": {
+ "name": "Wspierający",
+ "desc": "Dziękujemy za wsparcie!",
+ "hint": "Zostań sponsorem na GitHub"
+ },
+ "legend": {
+ "name": "Legenda",
+ "desc": "10 różnych quizów ukończonych!",
+ "hint": "Ukończ 10 różnych quizów"
+ },
+ "rocket_start": {
+ "name": "Rakietowy Start",
+ "desc": "Ukończyłeś 5 quizów!",
+ "hint": "Ukończ 5 quizów"
+ },
+ "royalty": {
+ "name": "Royalty",
+ "desc": "Dotarłeś na szczyt rankingu!",
+ "hint": "Zajmij pierwsze miejsce w rankingu"
+ },
+ "code_wizard": {
+ "name": "Czarodziej Kodu",
+ "desc": "Opanowałeś 5 różnych tematów!",
+ "hint": "Ukończ 5 różnych tematów quizów"
+ },
+ "endless": {
+ "name": "Nieskończony",
+ "desc": "Zdobyłeś 1000+ punktów!",
+ "hint": "Zdobądź 1000 punktów"
+ },
+ "star_gazer": {
+ "name": "Gwiazdozbiór",
+ "desc": "Dodałeś gwiazdkę do repozytorium DevLovers na GitHub!",
+ "hint": "Dodaj gwiazdkę do naszego repozytorium GitHub"
+ },
+ "silver_patron": {
+ "name": "Srebrny Patron",
+ "desc": "Sponsorowałeś nas dwukrotnie — dziękujemy!",
+ "hint": "Sponsoruj DevLovers po raz drugi"
+ },
+ "golden_patron": {
+ "name": "Złoty Patron",
+ "desc": "Sponsorowałeś nas 3+ razy — legendarne!",
+ "hint": "Sponsoruj DevLovers po raz trzeci"
+ },
+ "night_owl": {
+ "name": "Nocna Sowa",
+ "desc": "Ukończyłeś quiz po północy!",
+ "hint": "Ukończ quiz po północy"
+ },
+ "centurion": {
+ "name": "Centurion",
+ "desc": "Osiągnąłeś 100 punktów!",
+ "hint": "Zdobądź 100 punktów"
+ },
+ "deep_diver": {
+ "name": "Głębinowiec",
+ "desc": "Średnia 80%+ przez 10+ quizów!",
+ "hint": "Utrzymaj średnią 80%+ w 10 lub więcej próbach"
+ }
+ }
}
},
"aiHelper": {
diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json
index dba35074..01de8ea0 100644
--- a/frontend/messages/uk.json
+++ b/frontend/messages/uk.json
@@ -204,6 +204,9 @@
"hurryUp": "Поспішайте!"
},
"result": {
+ "accuracy": "Точність",
+ "integrity": "Чесність",
+ "violationsLabel": "Порушення",
"correctAnswers": "правильних відповідей",
"timeUp": {
"title": "Час вийшов",
@@ -1081,24 +1084,56 @@
"metaDescription": "Відстежуйте свій прогрес та результати квізів",
"title": "Кабінет користувача",
"subtitle": "З поверненням на вашу навчальну площадку",
- "supportLink": "Підтримка та відгуки",
+ "supportLink": "Відгук",
"profile": {
+ "title": "Профіль",
+ "subtitle": "Керуйте своїм публічним профілем та налаштуваннями",
"defaultName": "Розробник",
"defaultRole": "користувач",
"sponsor": "Спонсор",
"becomeSponsor": "Стати спонсором",
+ "supportAgain": "Проявити більше любові",
"sponsorThanks": "Дякуємо за вашу підтримку!",
"sponsorMore": "Підтримати ще більше",
+ "points": "Очок",
+ "quizzesTaken": "Квізів",
+ "joined": "Приєднався",
+ "globalRank": "Рейтинг",
+ "dayStreak": "День поспіль",
+ "daysStreak": "Дні поспіль",
"totalPoints": "Загальна кількість балів",
- "joined": "Приєднався"
+ "settings": "Налаштування",
+ "editProfile": "Редагувати профіль",
+ "changeName": "Змінити ім'я",
+ "saveChanges": "Зберегти зміни",
+ "changePassword": "Змінити пароль",
+ "currentPassword": "Поточний пароль",
+ "newPassword": "Новий пароль",
+ "saving": "Збереження..."
},
"stats": {
"title": "Статистика квізів",
- "noActivity": "Готові прокачатися? Випробуйте себе в новому квізі",
- "startQuiz": "Почати квіз",
+ "scoreDistribution": "Розподіл балів",
+ "scoreDistributionSubtext": "На основі ваших останніх спроб",
+ "activityHeatmap": "Графік активності",
"attempts": "Спроби",
"avgScore": "Середній бал",
- "continueLearning": "Продовжити навчання"
+ "mastered": "Засвоєно",
+ "review": "Потребує повторення",
+ "study": "Вивчення",
+ "totalAttempts": "Всього спроб",
+ "noActivity": "Ви ще не проходили жодного квізу. Почніть навчання та відстежуйте свій прогрес тут!",
+ "startQuiz": "Почати перший квіз",
+ "continueLearning": "Продовжити навчання",
+ "less": "Менше",
+ "more": "Більше",
+ "attemptsInPeriod": "{count} спроб за період",
+ "last3Months": "Останні 3 місяці",
+ "last5Months": "Останні 5 місяців",
+ "last6Months": "Останні 6 місяців",
+ "lastYear": "Цей рік",
+ "totalActiveDays": "Активні дні",
+ "mostActiveMonth": "Активний місяць"
},
"quizSaved": {
"title": "Результат квізу збережено!",
@@ -1114,6 +1149,7 @@
"noAttempts": "Ви ще не проходили жодного квізу",
"startQuiz": "Спробувати",
"score": "Результат",
+ "accuracy": "Точність",
"integrity": "Чистота",
"points": "Балів",
"scoreHint": "Кількість правильних відповідей з загальної кількості",
@@ -1167,9 +1203,112 @@
"messagePlaceholder": "Розкажіть, що ви думаєте...",
"submit": "Надіслати відгук",
"submitting": "Надсилання...",
+ "attachFile": "Прикріпити файл",
"success": "Дякуємо! Ваш відгук надіслано.",
"error": "Щось пішло не так. Спробуйте ще раз.",
"requiredField": "Будь ласка, заповніть це поле."
+ },
+ "achievements": {
+ "title": "Досягнення",
+ "subtitle": "{earned} з {total} отримано",
+ "ui": {
+ "expand": "Переглянути всі",
+ "collapse": "Згорнути",
+ "clickInfo": "Натисніть для інфо",
+ "clickBack": "Натисніть, щоб повернутись"
+ },
+ "badges": {
+ "first_blood": {
+ "name": "Перший крок",
+ "desc": "Завершив перший квіз!",
+ "hint": "Завершіть 1 квіз"
+ },
+ "sharpshooter": {
+ "name": "Снайпер",
+ "desc": "100% у квізі!",
+ "hint": "Отримайте 100% у будь-якому квізі"
+ },
+ "on_a_roll": {
+ "name": "У потоці",
+ "desc": "Завершив 3 квізи!",
+ "hint": "Завершіть 3 квізи"
+ },
+ "big_brain": {
+ "name": "Великий мозок",
+ "desc": "Завершив 10 квізів!",
+ "hint": "Завершіть 10 квізів"
+ },
+ "diamond_mind": {
+ "name": "Діамантовий розум",
+ "desc": "90%+ у 5 квізах!",
+ "hint": "Отримайте 90%+ у 5 квізах"
+ },
+ "perfectionist": {
+ "name": "Перфекціоніст",
+ "desc": "3 ідеальні результати!",
+ "hint": "Отримайте 100% у 3 квізах"
+ },
+ "supporter": {
+ "name": "Підтримувач",
+ "desc": "Дякуємо за підтримку!",
+ "hint": "Станьте спонсором на GitHub"
+ },
+ "legend": {
+ "name": "Легенда",
+ "desc": "10 різних квізів завершено!",
+ "hint": "Завершіть 10 різних квізів"
+ },
+ "rocket_start": {
+ "name": "Ракетний старт",
+ "desc": "Завершив 5 квізів!",
+ "hint": "Завершіть 5 квізів"
+ },
+ "royalty": {
+ "name": "Роялті",
+ "desc": "Досяг вершини рейтингу!",
+ "hint": "Займіть перше місце в рейтингу"
+ },
+ "code_wizard": {
+ "name": "Чарівник коду",
+ "desc": "Опанував 5 різних тем!",
+ "hint": "Завершіть 5 різних тем квізів"
+ },
+ "endless": {
+ "name": "Нескінченний",
+ "desc": "Набрав 1000+ балів!",
+ "hint": "Наберіть 1000 балів"
+ },
+ "star_gazer": {
+ "name": "Зоряний глядач",
+ "desc": "Ти додав зірочку репозиторію DevLovers на GitHub!",
+ "hint": "Постав зірочку нашому GitHub репозиторію"
+ },
+ "silver_patron": {
+ "name": "Срібний патрон",
+ "desc": "Ти спонсорував нас двічі — дякуємо!",
+ "hint": "Спонсоруй DevLovers вдруге"
+ },
+ "golden_patron": {
+ "name": "Золотий патрон",
+ "desc": "Ти спонсорував нас 3+ рази — легендарно!",
+ "hint": "Спонсоруй DevLovers втретє"
+ },
+ "night_owl": {
+ "name": "Нічна сова",
+ "desc": "Пройшов квіз після півночі!",
+ "hint": "Пройди квіз після опівночі"
+ },
+ "centurion": {
+ "name": "Центуріон",
+ "desc": "Досяг 100 балів!",
+ "hint": "Наберіть 100 балів"
+ },
+ "deep_diver": {
+ "name": "Глибокий ныряльщик",
+ "desc": "Середній результат 80%+ за 10+ квізів!",
+ "hint": "Тримай середній результат 80%+ у 10+ спробах"
+ }
+ }
}
},
"aiHelper": {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c39f7f1b..c432f385 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.1",
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
+ "@phosphor-icons/react": "^2.1.10",
"@portabletext/react": "^5.0.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-radio-group": "^1.3.8",
@@ -3401,6 +3402,18 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@phosphor-icons/react": {
+ "version": "2.1.10",
+ "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
+ "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8",
+ "react-dom": ">= 16.8"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8fdd0d6a..275b95e7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,6 +24,7 @@
},
"dependencies": {
"@neondatabase/serverless": "^1.0.2",
+ "@phosphor-icons/react": "^2.1.10",
"@portabletext/react": "^5.0.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-radio-group": "^1.3.8",
|