diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e5851c0..b6c570d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -42,6 +42,7 @@
"zustand": "^5.0.2"
},
"devDependencies": {
+ "@playwright/test": "^1.64.1",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
@@ -796,6 +797,22 @@
"node": ">= 10"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
+ "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.64.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz",
@@ -2973,6 +2990,35 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
+ "node_modules/playwright": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
+ "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.64.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.64.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
+ "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.28",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 1306a2b..8cca361 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -46,6 +46,7 @@
"zustand": "^5.0.2"
},
"devDependencies": {
+ "@playwright/test": "^1.64.1",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png
new file mode 100644
index 0000000..68f02f9
Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index 194480e..c1ea4f4 100644
Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ
diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png
new file mode 100644
index 0000000..f8ab2ad
Binary files /dev/null and b/frontend/public/icon-192.png differ
diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png
new file mode 100644
index 0000000..9c52165
Binary files /dev/null and b/frontend/public/icon-512.png differ
diff --git a/frontend/public/logo.png b/frontend/public/logo.png
index 47c3d57..17069a6 100644
Binary files a/frontend/public/logo.png and b/frontend/public/logo.png differ
diff --git a/frontend/src/app/cap/page.tsx b/frontend/src/app/cap/page.tsx
index 68c6525..748ef26 100644
--- a/frontend/src/app/cap/page.tsx
+++ b/frontend/src/app/cap/page.tsx
@@ -73,13 +73,6 @@ function CapPageInner() {
enabled: !!user && curriculumDone,
});
- useEffect(() => {
- if (!curriculumDone || !user) return;
- void http
- .post("/api/me/certificate/issue", {})
- .then(() => qc.invalidateQueries({ queryKey: ["me", "certificate"] }))
- .catch((e) => toast.error(toUserError(e, "صدور گواهینامه ممکن نشد")));
- }, [curriculumDone, user, qc]);
if (isLoading) {
return
- {chapters.length} فصل · {lessons.length} درس · ترتیب با کشیدن یا فلش · موقعیت x,y -
+ + {/* Course overview header */} +دوره آموزشی
+{ch.description}
} +{ch.title}
+ {ch.description && ( +{ch.description}
+ )}هنوز درسی نیست. «+ درس» را بزنید.
- ) : ( -{l.title}
-+ {l.title} +
+هنوز فصلی نیست
-با «فصل جدید» شروع کنید؛ درسهای هر فصل روی درخت مهارت هنرجو نمایش داده میشود.
+هنوز فصلی نیست
++ با «فصل جدید» شروع کنید؛ درسهای هر فصل روی درخت مهارت هنرجو نمایش داده میشود. +
+
{toUserError(overviewQ.error ?? studentsQ.error, "بارگذاری داشبورد یادگیری ممکن نشد")}
diff --git a/frontend/src/components/admin/site-settings-panel.tsx b/frontend/src/components/admin/site-settings-panel.tsx
index 353ae50..7fdbd26 100644
--- a/frontend/src/components/admin/site-settings-panel.tsx
+++ b/frontend/src/components/admin/site-settings-panel.tsx
@@ -1,21 +1,139 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useEffect, useState } from "react";
-import { Heart, Loader2, Package, Save, Settings2, Share2, Sparkles } from "lucide-react";
-import { http, toUserError } from "@/lib/api";
+import { useEffect, useRef, useState } from "react";
+import { Camera, Heart, Loader2, Package, Save, Settings2, Share2, Sparkles, Trash2, Users } from "lucide-react";
+import { apiForm, http, toUserError } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
+import { TagInput } from "@/components/ui/tag-input";
+import { AvatarCropDialog } from "@/components/avatar-crop";
import { toast } from "@/components/providers";
import type { AdminTabId } from "@/components/admin/nav";
+import { cn } from "@/lib/utils";
type SettingsMap = Record {error}
+ هنوز مشارکتکنندهای ثبت نشده. با «افزودن» شروع کنید.
+
رمزها، دیتابیس و JWT همچنان در فایل محیط (.env) میمانند — اینجا فقط تنظیمات قابلتغییر
روزمره است.
diff --git a/frontend/src/components/community-panel.tsx b/frontend/src/components/community-panel.tsx
index a7ac01c..376c49a 100644
--- a/frontend/src/components/community-panel.tsx
+++ b/frontend/src/components/community-panel.tsx
@@ -1,17 +1,50 @@
"use client";
import { useQuery } from "@tanstack/react-query";
-import { ExternalLink, Share2 } from "lucide-react";
+import { ExternalLink, Share2, Tag, Users } from "lucide-react";
import { http, toUserError } from "@/lib/api";
import { Panel } from "@/components/panel";
import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import type { Contributor } from "@/lib/types";
export type SocialLink = { name: string; url: string };
export type CommunityPayload = {
socials: SocialLink[];
+ contributors: Contributor[];
};
+function ContributorAvatar({ contributor }: { contributor: Contributor }) {
+ if (contributor.avatar) {
+ return (
+ {toUserError(error)} اطلاعات مشارکتکنندگان هنوز ثبت نشده است. {c.bio} {toUserError(error)}
+ ) : (
+
+ {initials}
+
+ )}
+ {uploading && (
+
+
{
+ (e.currentTarget as HTMLImageElement).style.display = "none";
+ (e.currentTarget.nextElementSibling as HTMLElement | null)?.removeAttribute("hidden");
+ }}
+ />
+ );
+ }
+ const initials = contributor.name
+ .split(" ")
+ .map((w) => w[0])
+ .slice(0, 2)
+ .join("")
+ .toUpperCase();
+ return (
+
+ {initials}
+
+ );
+}
+
export function CommunityPanel() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ["community"],
@@ -19,12 +52,17 @@ export function CommunityPanel() {
});
if (isLoading) {
- return
+ {contributors.map((c, i) => (
+
+ )}
+