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
2 changes: 2 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"organization.des": "Organizer introduction",
"organization.defaultDes": "This organizer is lazy and hasn't written any introduction.",
"organization.passedEvent": "Past conventions",
"pageview.label": "Views",
"pageview.aria": "{{count}} page views",
"years.title": "Overview",
"years.des": "FCC has documented {{totalAmount}} furry cons across {{totalYear}} years:",
"years.unknown": "Not yet scheduled",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"organization.des": "Об организаторе",
"organization.defaultDes": "Организатор ленив и не написал описание",
"organization.passedEvent": "Прошедшие фурконы",
"pageview.label": "Интерес",
"pageview.aria": "{{count}} просмотров страницы",
"years.title": "Итоги",
"years.des": "За {{totalYear}} лет в календаре фурри-мероприятий собрано {{totalAmount}} фурконов/встреч. Среди них:",
"years.unknown": "Дата уточняется",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/zh-Hans/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"organization.des": "展方简介",
"organization.defaultDes": "这个主办方很懒,什么介绍也没写过。",
"organization.passedEvent": "历届展会",
"pageview.label": "热度",
"pageview.aria": "本页累计浏览 {{count}} 次",
"years.title": "总结",
"years.des": "兽展日历共在 {{totalYear}} 年里收录到 {{totalAmount}} 场兽展/兽聚。 其中:",
"years.unknown": "暂未定档",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/zh-Hant/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"organization.des": "主辦單位簡介",
"organization.defaultDes": "這個主辦單位尚未提供簡介。",
"organization.passedEvent": "歷年活動",
"pageview.label": "熱度",
"pageview.aria": "本頁累計瀏覽 {{count}} 次",
"years.title": "年度統計",
"years.des": "獸展日曆共收錄 {{totalYear}} 個年度的 {{totalAmount}} 場展會/獸聚。其中:",
"years.unknown": "日期未定",
Expand Down
28 changes: 28 additions & 0 deletions src/api/infra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import API from "@/api";

type PageviewMetrics = {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
};

type PageviewReport = PageviewMetrics & {
comparison: PageviewMetrics;
};

type GetPageviewResponse = {
success: boolean;
pageCount?: PageviewReport;
};

export class InfraAPI {
static async getPageview(path: string) {
const response = await API.get<GetPageviewResponse>("internal/infra/pageview", {
params: { path },
});

return response.data;
}
}
24 changes: 24 additions & 0 deletions src/components/PageviewHeatTag/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from "clsx";
import { useTranslation } from "next-i18next/pages";
import { FaFire } from "react-icons/fa6";

function formatCount(count: number, locale: string): string {
const intlLocale = locale === "en" ? "en-US" : locale === "zh-Hant" ? "zh-Hant" : "zh-Hans";
return new Intl.NumberFormat(intlLocale, {
notation: count >= 10_000 ? "compact" : "standard",
maximumFractionDigits: 1,
}).format(count);
Comment on lines +6 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

修复 locale 映射默认分支,避免俄语页面被格式化成中文数字样式。

当前分支把所有非 en/zh-Hant 语言都强制映射到 zh-Hans;新增 ru 后会出现错误本地化展示。

💡 建议修改
 function formatCount(count: number, locale: string): string {
-  const intlLocale = locale === "en" ? "en-US" : locale === "zh-Hant" ? "zh-Hant" : "zh-Hans";
+  const intlLocaleMap: Record<string, string> = {
+    en: "en-US",
+    "zh-Hant": "zh-Hant",
+    "zh-Hans": "zh-Hans",
+    ru: "ru-RU",
+  };
+  const intlLocale = intlLocaleMap[locale] ?? locale;
   return new Intl.NumberFormat(intlLocale, {
     notation: count >= 10_000 ? "compact" : "standard",
     maximumFractionDigits: 1,
   }).format(count);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PageviewHeatTag/index.tsx` around lines 6 - 10, The locale
mapping for number formatting is too broad: intlLocale currently maps any
non-"en"/"zh-Hant" to "zh-Hans", causing languages like "ru" to be formatted
with Chinese rules; update the intlLocale logic (the locale variable and
intlLocale constant used with new Intl.NumberFormat) to only map explicit known
overrides (e.g., map "en" -> "en-US", "zh-Hant" -> "zh-Hant", "zh-Hans" ->
"zh-Hans") and otherwise pass through the incoming locale (or a safe fallback
like "en-US") so Russian ("ru") and other locales use their correct formatting.

}

export default function PageviewTag({ className, count }: { className?: string; count: number | null }) {
const { t, i18n } = useTranslation();

if (count == null) return null;

return (
<span className={clsx("inline-flex items-center", className)} aria-label={t("pageview.aria", { count })}>
<FaFire className="text-sm shrink-0" aria-hidden />
<span className="tabular-nums leading-none ml-1">{formatCount(count, i18n.language)}</span>
</span>
);
}
62 changes: 41 additions & 21 deletions src/pages/[organization]/[slug].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { InfraAPI } from "@/api/infra";
import { EventsAPI } from "@/api/events";
import EventMapCard from "@/components/event/EventMapCard";
import EventOrganizationCard from "@/components/event/EventOrganizationCard";
import EventSourceButton from "@/components/event/EventSourceButton";
import { EventDate } from "@/components/eventCard";
import NextImage from "@/components/image";
import PageviewTag from "@/components/PageviewHeatTag";
import { EventStatus } from "@/constants/event";
import type { EventItem } from "@/types/event";
import { getEventCoverImgPath, imageUrl } from "@/utils/imageLoader";
Expand All @@ -20,8 +22,9 @@ import { FaHotel, FaPeoplePulling } from "react-icons/fa6";
import { IoLocation } from "react-icons/io5";
import { RiErrorWarningLine } from "react-icons/ri";
import * as z from "zod/v4";
import axios from "axios";

export default function EventDetail({ event }: { event: EventItem }) {
export default function EventDetail({ event, pageviewCount }: { event: EventItem; pageviewCount: number | null }) {
const { t, i18n } = useTranslation();

const finalEventCoverImage = getEventCoverImgPath(event);
Expand Down Expand Up @@ -67,19 +70,20 @@ export default function EventDetail({ event }: { event: EventItem }) {
</p>
)}

<h2 aria-label={t("event.aria.name")} className="font-bold text-3xl text-gray-700">
{event.name}
</h2>
<h2 className="text-gray-600 text-sm flex">
<div className="flex flex-wrap items-baseline gap-2 gap-y-1">
<h2 aria-label={t("event.aria.name")} className="font-bold text-3xl text-gray-700">
{event.name}
</h2>
</div>
<h2 className="text-gray-600 text-sm flex items-center gap-2">
{t("event.hostBy", {
hostName: event.organizations.length
? [
event.organization?.name ?? "",
...event.organizations.map((organization) => organization.name),
].filter(Boolean).join("、")
? [event.organization?.name ?? "", ...event.organizations.map((organization) => organization.name)]
.filter(Boolean)
.join("、")
: event.organization?.name,
})}
{/* <EventStatusBar className="ml-2" pageviews="0" fav="2" /> */}
<PageviewTag count={pageviewCount} />
</h2>

<p aria-label={t("event.aria.location")} className="flex items-center text-gray-500 mt-4">
Expand Down Expand Up @@ -164,7 +168,8 @@ export default function EventDetail({ event }: { event: EventItem }) {

export async function getServerSideProps(context: GetServerSidePropsContext) {
try {
const { locale } = context;
const { locale: localeParam } = context;
const appLocale = formatLocale(localeParam);

const eventParamsSchema = z.object({
slug: z
Expand All @@ -182,7 +187,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
organization: context.params?.organization,
});

const event = await EventsAPI.getEventDetail(reqParamsParseResult.slug, reqParamsParseResult.organization);
const pageviewPath = getEventDetailUrl({
eventSlug: reqParamsParseResult.slug,
organizationSlug: reqParamsParseResult.organization,
locale: appLocale,
fullUrl: false,
});

const [event, pv] = await Promise.all([
EventsAPI.getEventDetail(reqParamsParseResult.slug, reqParamsParseResult.organization),
InfraAPI.getPageview(pageviewPath).catch((): null => null),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!event) {
return {
Expand All @@ -193,37 +208,42 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
event: event,
pageviewCount: pv?.pageCount?.pageviews ?? null,
headMetas: {
title: `${event?.name}-${event?.organization?.name}`,
keywords: keywordGenerator({
page: "event",
locale: formatLocale(locale),
locale: appLocale,
event: {
name: event?.name,
startDate: event?.startAt,
city: event.region?.localName || undefined,
},
}),
des: eventDescriptionGenerator(formatLocale(locale), event),
des: eventDescriptionGenerator(appLocale, event),
url: getEventDetailUrl({
eventSlug: event.slug,
organizationSlug: event.organization.slug,
locale: formatLocale(locale),
locale: appLocale,
fullUrl: false,
}),
cover: imageUrl(getEventCoverImgPath(event)),
},
structuredData: generateEventDetailStructuredData({
event,
locale: formatLocale(locale),
locale: appLocale,
}),
...(locale ? await serverSideTranslations(locale, ["common"]) : {}),
...(localeParam ? await serverSideTranslations(localeParam, ["common"]) : {}),
},
};
} catch (error) {
console.error(error);
return {
notFound: true,
};
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
return {
notFound: true,
};
}
}
throw error;
}
}
47 changes: 33 additions & 14 deletions src/pages/[organization]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import OrganizationStatus from "@/components/organizationStatus";
import styles from "@/styles/Organization.module.css";
import clsx from "clsx";
import Image from "@/components/image";
import PageviewTag from "@/components/PageviewHeatTag";
import { GetServerSidePropsContext } from "next/types";
import { useMemo } from "react";
import toast from "react-hot-toast";
Expand All @@ -16,12 +17,14 @@ import "dayjs/locale/zh-tw";
import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
import { useTranslation } from "next-i18next/pages";
import { OrganizationsAPI } from "@/api/organizations";
import { InfraAPI } from "@/api/infra";
import * as z from "zod/v4";
import type { EventCardItem } from "@/types/event";
import type { Organization } from "@/types/organization";
import { keywordGenerator, organizationDetailDescriptionGenerator, OrganizationPageMeta } from "@/utils/meta";
import { getOrganizationDetailUrl } from "@/utils/url";
import { breadcrumbGenerator } from "@/utils/structuredData";
import { currentSupportLocale, getDayjsLocale } from "@/utils/locale";
import { formatLocale, getDayjsLocale } from "@/utils/locale";
import axios from "axios";
// import {
// WebsiteButton,
Expand All @@ -35,9 +38,13 @@ import axios from "axios";

dayjs.extend(relativeTime);

export default function OrganizationDetail(props: { events: EventCardItem[]; organization: Organization }) {
export default function OrganizationDetail(props: {
events: EventCardItem[];
organization: Organization;
pageviewCount: number | null;
}) {
const { t, i18n } = useTranslation();
const { organization, events } = props;
const { organization, events, pageviewCount } = props;
const dayjsLocale = getDayjsLocale(i18n.language);

const formattedFirstEventTime = useMemo(() => {
Expand Down Expand Up @@ -88,10 +95,13 @@ export default function OrganizationDetail(props: { events: EventCardItem[]; org
</div>
)}
<div className="mt-4 md:mt-0 md:ml-4 ">
<h2 className="text-2xl font-bold mb-2">{organization.name}</h2>
<div className="flex flex-wrap items-center gap-2 mb-2">
<h2 className="text-2xl font-bold">{organization.name}</h2>
</div>

<div className="flex items-center mb-2 text-gray-500">
<div className="flex items-center gap-2 mb-2 text-gray-500">
<OrganizationStatus status={organization.status} />
<PageviewTag count={pageviewCount} />
</div>

<div className={clsx("mb-2 text-gray-500", styles["intro-bar"])}>
Expand Down Expand Up @@ -298,7 +308,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});

try {
const data = await OrganizationsAPI.getOrganizationDetail(reqParamsParseResult.organization);
const locale = formatLocale(context.locale);
const pageviewPath = getOrganizationDetailUrl({
organizationSlug: reqParamsParseResult.organization,
locale,
fullUrl: false,
});

const [data, pv] = await Promise.all([
OrganizationsAPI.getOrganizationDetail(reqParamsParseResult.organization),
InfraAPI.getPageview(pageviewPath).catch((): null => null),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!data) {
return {
notFound: true,
};
}

const validOrganization = data.organization;
const validEvents: EventCardItem[] =
Expand Down Expand Up @@ -328,18 +354,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return 0;
}) || [];

if (!data) {
return {
notFound: true,
};
}

const locale = (context.locale as currentSupportLocale) || "zh-Hans";

return {
props: {
organization: validOrganization,
events: validEvents,
pageviewCount: pv?.pageCount?.pageviews ?? null,
headMetas: {
title: `${validOrganization?.name}`,
des: organizationDetailDescriptionGenerator(
Expand Down
Loading