's'
This commit is contained in:
@@ -601,6 +601,12 @@ const SOCIAL_MODE_META: Record<SocialMode, { title: string; subtitle: string; ba
|
||||
},
|
||||
};
|
||||
|
||||
const DATING_INTENT_BY_MODE: Record<SocialMode, string> = {
|
||||
coffeechat: "friends",
|
||||
localGuide: "partner",
|
||||
cityWalk: "explore",
|
||||
};
|
||||
|
||||
function SocialMembersPanel({
|
||||
city,
|
||||
mode,
|
||||
@@ -611,6 +617,7 @@ function SocialMembersPanel({
|
||||
members: CitySocialMember[];
|
||||
}) {
|
||||
const meta = SOCIAL_MODE_META[mode];
|
||||
const datingHref = `/dating?intent=${DATING_INTENT_BY_MODE[mode]}&city=${encodeURIComponent(city.slug || city.name)}`;
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<section className={`rounded-2xl border border-gray-200 p-4 dark:border-gray-700 ${meta.tone}`}>
|
||||
@@ -621,7 +628,7 @@ function SocialMembersPanel({
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 opacity-90">{meta.subtitle}</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dating"
|
||||
href={datingHref}
|
||||
className="rounded-full bg-gray-900 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200"
|
||||
>
|
||||
{meta.cta}
|
||||
|
||||
@@ -63,6 +63,9 @@ export default function NavbarComponent() {
|
||||
const isActive = useCallback((href: string) => {
|
||||
if (href.startsWith("/#")) return false;
|
||||
if (href === "/") return pathname === "/";
|
||||
if (href === "/digital") {
|
||||
return /^\/(zh|en)\/digital(?:\/|$)/.test(pathname);
|
||||
}
|
||||
return pathname.startsWith(href);
|
||||
}, [pathname]);
|
||||
|
||||
|
||||
203
app/components/meetups/EventLiveRoom.tsx
Normal file
203
app/components/meetups/EventLiveRoom.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import {
|
||||
MIROTALK_PROVIDER_LABEL,
|
||||
WEB_CHAT_PROVIDER_LABEL,
|
||||
type MeetupSession,
|
||||
} from "@/app/lib/meetups";
|
||||
|
||||
interface EventLiveRoomProps {
|
||||
locale: string;
|
||||
meetup: {
|
||||
id: string;
|
||||
city: string;
|
||||
emoji: string;
|
||||
venue: string;
|
||||
date: string;
|
||||
time: string;
|
||||
organizer: string;
|
||||
gradientFrom: string;
|
||||
gradientTo: string;
|
||||
};
|
||||
session: MeetupSession;
|
||||
formattedDate: string;
|
||||
}
|
||||
|
||||
const copy = {
|
||||
zh: {
|
||||
back: "返回活动列表",
|
||||
video: "视频会议",
|
||||
chat: "Web 群聊",
|
||||
split: "分屏",
|
||||
videoOnly: "只看视频",
|
||||
chatOnly: "只看群聊",
|
||||
identity: "社区身份",
|
||||
guest: "访客",
|
||||
vip: "VIP",
|
||||
member: "成员",
|
||||
channel: "活动频道",
|
||||
openChat: "新窗口打开群聊",
|
||||
openVideo: "新窗口打开视频",
|
||||
locked: "当前权限不足,无法进入活动间",
|
||||
login: "登录社区账户",
|
||||
joinVip: "开通 VIP",
|
||||
providers: "服务由 VPS 自建开源项目提供",
|
||||
},
|
||||
en: {
|
||||
back: "Back to events",
|
||||
video: "Video",
|
||||
chat: "Web chat",
|
||||
split: "Split",
|
||||
videoOnly: "Video only",
|
||||
chatOnly: "Chat only",
|
||||
identity: "Community identity",
|
||||
guest: "Guest",
|
||||
vip: "VIP",
|
||||
member: "Member",
|
||||
channel: "Event channel",
|
||||
openChat: "Open chat in new tab",
|
||||
openVideo: "Open video in new tab",
|
||||
locked: "You do not have access to this live room yet.",
|
||||
login: "Sign in",
|
||||
joinVip: "Get VIP",
|
||||
providers: "Powered by self-hosted open-source services on VPS",
|
||||
},
|
||||
};
|
||||
|
||||
type PanelMode = "split" | "video" | "chat";
|
||||
|
||||
export default function EventLiveRoom({ locale, meetup, session, formattedDate }: EventLiveRoomProps) {
|
||||
const c = locale === "zh" ? copy.zh : copy.en;
|
||||
const [panel, setPanel] = useState<PanelMode>("split");
|
||||
const badge = useMemo(() => {
|
||||
if (session.user?.vip) return c.vip;
|
||||
if (session.user) return c.member;
|
||||
return c.guest;
|
||||
}, [c.guest, c.member, c.vip, session.user]);
|
||||
|
||||
if (!session.canJoin) {
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[60vh] max-w-2xl flex-col items-center justify-center px-4 text-center">
|
||||
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{c.locked}</p>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/login"
|
||||
className="rounded-xl bg-[#ff4d4f] px-5 py-2.5 text-sm font-semibold text-white hover:bg-[#ff7a45]"
|
||||
>
|
||||
{c.login}
|
||||
</Link>
|
||||
{session.lockReason === "vip_required" ? (
|
||||
<Link
|
||||
href="/join"
|
||||
className="rounded-xl border border-violet-300 px-5 py-2.5 text-sm font-semibold text-violet-700 dark:border-violet-700 dark:text-violet-300"
|
||||
>
|
||||
{c.joinVip}
|
||||
</Link>
|
||||
) : null}
|
||||
<Link href="/meetups" className="rounded-xl border border-gray-200 px-5 py-2.5 text-sm font-semibold text-gray-600 dark:border-gray-700 dark:text-gray-300">
|
||||
{c.back}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const showVideo = panel === "split" || panel === "video";
|
||||
const showChat = panel === "split" || panel === "chat";
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-[#0b1220] text-white">
|
||||
<header
|
||||
className="border-b border-white/10 px-4 py-4 sm:px-6"
|
||||
style={{ background: `linear-gradient(135deg, ${meetup.gradientFrom}, ${meetup.gradientTo})` }}
|
||||
>
|
||||
<div className="mx-auto flex max-w-[1600px] flex-wrap items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<Link href="/meetups" className="text-xs font-medium text-white/75 hover:text-white">
|
||||
← {c.back}
|
||||
</Link>
|
||||
<h1 className="mt-2 truncate text-xl font-bold sm:text-2xl">
|
||||
{meetup.emoji} {meetup.city} · {meetup.venue}
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-white/85">{formattedDate}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-full bg-black/20 px-3 py-1 text-xs font-semibold backdrop-blur">
|
||||
{c.identity}: {session.displayName}
|
||||
</span>
|
||||
<span className="rounded-full bg-black/20 px-3 py-1 text-xs font-semibold backdrop-blur">{badge}</span>
|
||||
<span className="rounded-full bg-black/20 px-3 py-1 text-xs font-semibold backdrop-blur">
|
||||
{c.channel} {session.loungeChannel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="border-b border-white/10 bg-[#111827] px-4 py-3 sm:px-6">
|
||||
<div className="mx-auto flex max-w-[1600px] flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-2 text-xs text-white/70">
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-1">{MIROTALK_PROVIDER_LABEL}</span>
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-1">{WEB_CHAT_PROVIDER_LABEL}</span>
|
||||
<span className="rounded-full bg-white/10 px-2.5 py-1">{c.providers}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["split", "video", "chat"] as PanelMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
onClick={() => setPanel(mode)}
|
||||
className={`rounded-lg px-3 py-1.5 text-xs font-semibold transition ${
|
||||
panel === mode ? "bg-[#ff4d4f] text-white" : "bg-white/10 text-white/80 hover:bg-white/15"
|
||||
}`}
|
||||
>
|
||||
{mode === "split" ? c.split : mode === "video" ? c.videoOnly : c.chatOnly}
|
||||
</button>
|
||||
))}
|
||||
<a
|
||||
href={session.videoUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-lg border border-white/15 px-3 py-1.5 text-xs font-semibold text-white/85 hover:bg-white/10"
|
||||
>
|
||||
{c.openVideo}
|
||||
</a>
|
||||
<a
|
||||
href={session.chatUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-lg border border-white/15 px-3 py-1.5 text-xs font-semibold text-white/85 hover:bg-white/10"
|
||||
>
|
||||
{c.openChat}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="mx-auto grid w-full max-w-[1600px] flex-1 gap-0 px-0 py-0 lg:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]">
|
||||
{showVideo ? (
|
||||
<section className={`relative min-h-[320px] bg-black ${panel === "split" ? "border-r border-white/10" : ""}`}>
|
||||
<iframe
|
||||
title={`${meetup.city} video`}
|
||||
src={session.videoUrl}
|
||||
className="h-[50vh] w-full lg:h-[calc(100vh-180px)]"
|
||||
allow="camera; microphone; display-capture; fullscreen; clipboard-read; clipboard-write"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
{showChat ? (
|
||||
<section className="relative min-h-[320px] bg-[#0f172a]">
|
||||
<iframe
|
||||
title={`${meetup.city} chat`}
|
||||
src={session.chatUrl}
|
||||
className="h-[50vh] w-full lg:h-[calc(100vh-180px)]"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user