Skip to content

Commit 9c12622

Browse files
feat(community): member profiles + Telegram join gate
Directory/profile UX with carrot level meter; mute new group members until site profile complete, then unmute. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a0b794e commit 9c12622

18 files changed

Lines changed: 1433 additions & 166 deletions

File tree

‎.vscode/settings.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"cursor.openai.baseUrl": "http://127.0.0.1:8787/p/frontchapter/v1",
33
"deno.enable": true,
4-
"deno.enablePaths": ["supabase/functions"]
4+
"deno.enablePaths": ["supabase/functions"],
5+
"css.lint.unknownAtRules": "ignore",
6+
"scss.lint.unknownAtRules": "ignore"
57
}

‎scripts/set-telegram-webhook.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""Set Telegram webhook for FrontChapter group gate bot.
3+
4+
Usage:
5+
export TELEGRAM_BOT_TOKEN=...
6+
export TELEGRAM_WEBHOOK_SECRET=... # random string you also set as Supabase secret
7+
export SUPABASE_PROJECT_REF=yuwsoyhknbjipfouemen
8+
python3 scripts/set-telegram-webhook.py
9+
10+
Optional:
11+
TELEGRAM_GROUP_CHAT_ID=-100… (or let bot learn it on first join /chatid)
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import json
17+
import os
18+
import sys
19+
import urllib.parse
20+
import urllib.request
21+
22+
TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
23+
SECRET = os.environ.get("TELEGRAM_WEBHOOK_SECRET", "").strip()
24+
REF = os.environ.get("SUPABASE_PROJECT_REF", "yuwsoyhknbjipfouemen").strip()
25+
26+
if not TOKEN or not SECRET:
27+
print("Need TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET", file=sys.stderr)
28+
sys.exit(1)
29+
30+
url = f"https://{REF}.supabase.co/functions/v1/telegram-bot"
31+
payload = {
32+
"url": url,
33+
"secret_token": SECRET,
34+
"allowed_updates": ["chat_member", "message"],
35+
"drop_pending_updates": True,
36+
}
37+
38+
req = urllib.request.Request(
39+
f"https://api.telegram.org/bot{TOKEN}/setWebhook",
40+
data=json.dumps(payload).encode(),
41+
headers={"Content-Type": "application/json"},
42+
method="POST",
43+
)
44+
with urllib.request.urlopen(req) as res:
45+
body = json.loads(res.read().decode())
46+
print(json.dumps(body, indent=2, ensure_ascii=False))
47+
48+
info_req = urllib.request.Request(
49+
f"https://api.telegram.org/bot{TOKEN}/getWebhookInfo"
50+
)
51+
with urllib.request.urlopen(info_req) as res:
52+
print(json.dumps(json.loads(res.read().decode()), indent=2, ensure_ascii=False))

‎src/app/members/[slug]/page.tsx‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import GSAPWrapper from '@/src/layouts/components/GSAPWrapper';
2+
import MemberSingle, {
3+
buildMemberMetadata,
4+
} from '@/src/layouts/MemberSingle';
5+
import {
6+
getMemberActivities,
7+
getMemberTitles,
8+
getPublicMemberBySlug,
9+
listPublicMembers,
10+
staticParamsFromMembers,
11+
} from '@lib/membership/fetch';
12+
import type { Metadata } from 'next';
13+
import { notFound } from 'next/navigation';
14+
15+
type Params = { slug: string };
16+
17+
export async function generateStaticParams() {
18+
try {
19+
const members = await listPublicMembers();
20+
return staticParamsFromMembers(members);
21+
} catch {
22+
// ponytail: empty params if Supabase unreachable at build — directory still client-fetches
23+
return [];
24+
}
25+
}
26+
27+
export async function generateMetadata({
28+
params,
29+
}: {
30+
params: Params;
31+
}): Promise<Metadata> {
32+
try {
33+
const member = await getPublicMemberBySlug(params.slug);
34+
if (!member) return { title: 'هویجی یافت نشد' };
35+
return buildMemberMetadata(member);
36+
} catch {
37+
return { title: 'هویجی‌ها' };
38+
}
39+
}
40+
41+
const MemberPage = async ({ params }: { params: Params }) => {
42+
const member = await getPublicMemberBySlug(params.slug);
43+
if (!member) notFound();
44+
45+
const [activities, titles] = await Promise.all([
46+
getMemberActivities(member.id),
47+
getMemberTitles(member.id),
48+
]);
49+
50+
return (
51+
<GSAPWrapper>
52+
<main id="main-content">
53+
<MemberSingle
54+
member={member}
55+
activities={activities}
56+
titles={titles}
57+
/>
58+
</main>
59+
</GSAPWrapper>
60+
);
61+
};
62+
63+
export default MemberPage;

‎src/layouts/Join.tsx‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import Banner from '@layouts/components/Banner';
44
import {
55
CarrotBadge,
66
CarrotButton,
7+
CarrotLevel,
78
CarrotLoader,
89
CarrotSuccessState,
910
} from '@layouts/components/carrot';
1011
import {
11-
LEVEL_LABELS,
1212
levelFromPoints,
1313
type Member,
1414
type MemberStats,
@@ -279,6 +279,14 @@ const Join = () => {
279279
const updated = data as Member;
280280
setMember(updated);
281281
if (session) await loadMember(session.user.id);
282+
// Open Telegram group chat if they were muted pending profile
283+
try {
284+
await supabase.functions.invoke('telegram-bot', {
285+
body: { action: 'unmute' },
286+
});
287+
} catch {
288+
// non-fatal — user can still use the site
289+
}
282290
setStep('done');
283291
} catch (err) {
284292
setError(err instanceof Error ? err.message : 'ذخیره ناموفق بود');
@@ -550,28 +558,28 @@ const Join = () => {
550558
/>
551559
) : null}
552560
<div className="min-w-0 flex-1">
553-
<div className="mb-2 flex flex-wrap items-center gap-2">
561+
<div className="mb-2 flex flex-wrap items-center justify-center gap-3 sm:justify-start">
554562
<h2 className="h4 mb-0 text-dark">
555563
{member.display_name}
556564
</h2>
557-
<CarrotBadge accent>{LEVEL_LABELS[levelKey]}</CarrotBadge>
558565
</div>
559566
{member.username ? (
560567
<p className="mb-1 text-sm text-muted">
561568
@{member.username}
562569
</p>
563570
) : null}
564571
{member.expertise ? (
565-
<p className="mb-2 text-sm font-medium text-dark">
572+
<p className="mb-3 text-sm font-medium text-dark">
566573
{member.expertise}
567574
</p>
568575
) : null}
576+
<CarrotLevel level={levelKey} size="md" />
577+
<p className="mt-1 text-xs text-muted">{points} امتیاز</p>
569578
{member.bio ? (
570-
<p className="mb-3 text-sm leading-relaxed text-muted">
579+
<p className="mt-3 mb-0 text-sm leading-relaxed text-muted">
571580
{member.bio}
572581
</p>
573582
) : null}
574-
<p className="text-sm text-muted">{points} امتیاز</p>
575583
</div>
576584
</div>
577585

0 commit comments

Comments
 (0)