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: 1 addition & 1 deletion .github/workflows/deploy-prod-cn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
docker load -i image.tar

- name: Login to Qcloud Hongkong Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
Comment thread
PaiJi marked this conversation as resolved.
with:
registry: hkccr.ccs.tencentyun.com
username: "${{ vars.QCLOUD_REGISTRY_USERNAME }}"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/reusable-docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Create Env
run: |
Expand All @@ -47,7 +47,7 @@ jobs:
echo "Created .env file with provided secrets and variables"

- name: Build Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
Comment thread
PaiJi marked this conversation as resolved.
timeout-minutes: 10
with:
context: .
Expand Down
4 changes: 4 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"event.mapLoading": "Loading map",
"event.gotoGaoDe": "View on Gaode Map",
"event.gotoOrganization": "View Organizer details",
"event.hostSwitchGroupAria": "Switch host organization",
"event.prevHostOrganization": "Previous organizer",
"event.nextHostOrganization": "Next organizer",
"event.hostOrganizationCounter": "Organizer {{index}}/{{total}}",
"event.dateFormat": "MMMM DD, YYYY",
"organization.active": "Active Organizers",
"organization.inactive": "Inactive Organizers",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"event.mapLoading": "Открыть в Gaode Map...",
"event.gotoGaoDe": "Открыть в Gaode Map",
"event.gotoOrganization": "Участники и стенды",
"event.hostSwitchGroupAria": "Переключить организатора",
"event.prevHostOrganization": "Предыдущий организатор",
"event.nextHostOrganization": "Следующий организатор",
"event.hostOrganizationCounter": "Организатор {{index}}/{{total}}",
"event.dateFormat": "MMMM DD, YYYY",
"organization.active": "Активные организаторы",
"organization.inactive": "Неактивные организаторы",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/zh-Hans/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
"event.retryLoadMap": "尝试重新加载地图",
"event.gotoGaoDe": "去高德地图查看",
"event.gotoOrganization": "看看展商详情",
"event.hostSwitchGroupAria": "切换主办/展商",
"event.prevHostOrganization": "上一个主办",
"event.nextHostOrganization": "下一个主办",
"event.hostOrganizationCounter": "主办方 {{index}}/{{total}}",
"event.dateFormat": "YYYY年MM月DD日",
"organization.active": "活跃展商",
"organization.inactive": "停止活动展商",
Expand Down
4 changes: 4 additions & 0 deletions public/locales/zh-Hant/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
"event.retryLoadMap": "重新載入地圖",
"event.gotoGaoDe": "用高德地圖查看",
"event.gotoOrganization": "查看主辦單位",
"event.hostSwitchGroupAria": "切換主辦/展商",
"event.prevHostOrganization": "上一個主辦",
"event.nextHostOrganization": "下一個主辦",
"event.hostOrganizationCounter": "主辦方 {{index}}/{{total}}",
"event.dateFormat": "YYYY年MM月DD日",
"organization.active": "營運中",
"organization.inactive": "已停辦",
Expand Down
155 changes: 155 additions & 0 deletions src/components/event/EventOrganizationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import NextImage from "@/components/image";
import {
BiliButton,
EmailButton,
FacebookButton,
PlurkButton,
QQGroupButton,
RednoteButton,
TwitterButton,
WebsiteButton,
WeiboButton,
} from "@/components/OrganizationLinkButton";
import OrganizationStatus from "@/components/organizationStatus";
import type { EventItem } from "@/types/event";
import { sendTrack } from "@/utils/track";
import clsx from "clsx";
import { useTranslation } from "next-i18next/pages";
import Link from "next/link";
import { useState } from "react";
import { IoChevronBack, IoChevronForward } from "react-icons/io5";

type EventOrganizationCardProps = {
event: EventItem;
showDescriptionContainer: boolean;
};

export default function EventOrganizationCard(props: EventOrganizationCardProps) {
const { t } = useTranslation();
const orgList = [props.event.organization, ...props.event.organizations];
const [activeIndex, setActiveIndex] = useState(0);
const showOrgSwitcher = orgList.length > 1;

const organization = orgList[activeIndex] ?? orgList[0]!;
Comment on lines +29 to +33
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

orgList 可能重复且存在空值风险。

  • orgList = [props.event.organization, ...props.event.organizations] 未去重。若后端在 organizations 中已包含主办方本身,切换器会出现相同主办方重复出现的条目。
  • organization = orgList[activeIndex] ?? orgList[0]! 使用了非空断言,但若 event.organization 为空且 event.organizations 为空,orgList[0] 仍为 undefined,下方 organization.logoUrlorganization.slug 等访问将抛错。

建议按 slug/id 去重并对空情形提前返回:

♻️ 建议修改
-  const orgList = [props.event.organization, ...props.event.organizations];
+  const orgList = [props.event.organization, ...(props.event.organizations ?? [])]
+    .filter((o): o is NonNullable<typeof o> => !!o)
+    .filter((o, i, arr) => arr.findIndex((x) => x.slug === o.slug) === i);
+  if (orgList.length === 0) return null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const orgList = [props.event.organization, ...props.event.organizations];
const [activeIndex, setActiveIndex] = useState(0);
const showOrgSwitcher = orgList.length > 1;
const organization = orgList[activeIndex] ?? orgList[0]!;
const orgList = [props.event.organization, ...(props.event.organizations ?? [])]
.filter((o): o is NonNullable<typeof o> => !!o)
.filter((o, i, arr) => arr.findIndex((x) => x.slug === o.slug) === i);
if (orgList.length === 0) return null;
const [activeIndex, setActiveIndex] = useState(0);
const showOrgSwitcher = orgList.length > 1;
const organization = orgList[activeIndex] ?? orgList[0]!;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/event/EventOrganizationCard.tsx` around lines 29 - 33, orgList
can contain duplicates and undefined, and organization uses a non-null assertion
that can still be undefined; fix by building orgList from
props.event.organization and props.event.organizations after filtering out
null/undefined and deduplicating by a stable key (slug or id), e.g., dedupe by
slug/id, then set showOrgSwitcher = dedupedList.length > 1, compute organization
= dedupedList[activeIndex] ?? dedupedList[0] (without using !) and ensure
activeIndex is clamped/reset to 0 if it is out of bounds after dedupe; if
dedupedList is empty, return early (or render a safe placeholder) to avoid
accessing organization.logoUrl/slug.

const organizationProfileHref = `/${organization.slug}`;
const trackOrganizationProfileClick = () => {
sendTrack({
eventName: "click-event-portal",
eventValue: {
link: organizationProfileHref,
},
});
};

const goToPrev = () => {
setActiveIndex((i) => (i - 1 + orgList.length) % orgList.length);
};
const goToNext = () => {
setActiveIndex((i) => (i + 1) % orgList.length);
};

return (
<div
id="event-detail__right"
className={clsx(
"bg-white rounded-xl mb-4 lg:mb-0",
!props.showDescriptionContainer && "w-full",
props.showDescriptionContainer && "md:w-4/12",
)}
>
<div className="p-4">
{showOrgSwitcher && (
<div className="flex justify-between items-center mb-1">
<span className="text-lg font-bold text-gray-600">
{t("event.hostOrganizationCounter", { index: activeIndex + 1, total: orgList.length })}
</span>
<div
className="inline-flex p-1.5 gap-1 items-stretch rounded-xl bg-slate-100/95"
role="group"
aria-label={t("event.hostSwitchGroupAria")}
>
<button
type="button"
onClick={goToPrev}
className="rounded-lg px-2.5 py-1.5 text-slate-600 bg-white hover:shadow-sm transition-[color,box-shadow] duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300/80 focus-visible:ring-offset-1"
aria-label={t("event.prevHostOrganization")}
>
<IoChevronBack className="text-lg" aria-hidden />
</button>
<button
type="button"
onClick={goToNext}
className="rounded-lg px-2.5 py-1.5 text-slate-600 bg-white hover:shadow-sm transition-[color,box-shadow] duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-300/80 focus-visible:ring-offset-1"
aria-label={t("event.nextHostOrganization")}
>
<IoChevronForward className="text-lg" aria-hidden />
</button>
</div>
</div>
)}
<div className="flex">
{organization.logoUrl && (
<div className="border rounded flex justify-center items-center p-2 w-[100px] h-[100px]">
<NextImage
className="object-contain"
alt={t("organization.logoAlt", { name: organization.name })}
width={200}
height={200}
src={organization.logoUrl}
autoFormat
/>
</div>
)}
<div className="ml-4 flex flex-col justify-between">
<div>
<Link
className="text-2xl font-bold text-gray-600"
href={organizationProfileHref}
onClick={trackOrganizationProfileClick}
>
{organization.name}
</Link>
<div className="flex items-center text-gray-500 mb-4">
<span className="text-sm">
<OrganizationStatus status={organization.status || ""} />
</span>
</div>
</div>

<Link href={organizationProfileHref} onClick={trackOrganizationProfileClick}>
<button
type="button"
className="border rounded px-2 py-1 text-sm text-gray-500 hover:border-slate-400 hover:drop-shadow transition duration-200"
>
{t("event.gotoOrganization")}
</button>
</Link>
</div>
</div>

<div
className={clsx(
"items-center text-gray-500 grid gap-4 mt-4",
!props.showDescriptionContainer && "lg:grid-cols-2",
)}
>
{organization.website && <WebsiteButton t={t} href={organization.website} />}
{organization.qqGroup && <QQGroupButton t={t} text={organization.qqGroup} />}
{organization.bilibili && <BiliButton t={t} href={organization.bilibili} />}

{organization.weibo && <WeiboButton t={t} href={organization.weibo} />}

{organization.twitter && <TwitterButton t={t} href={organization.twitter} />}

{organization.contactMail && <EmailButton t={t} mail={organization.contactMail} />}

{organization.plurk && <PlurkButton t={t} href={organization.plurk} />}

{organization.facebook && <FacebookButton t={t} href={organization.facebook} />}

{organization.rednote && <RednoteButton t={t} href={organization.rednote} />}
</div>
</div>
</div>
);
}
105 changes: 10 additions & 95 deletions src/pages/[organization]/[slug].tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,20 @@
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 {
BiliButton,
EmailButton,
FacebookButton,
PlurkButton,
QQGroupButton,
RednoteButton,
TwitterButton,
WebsiteButton,
WeiboButton,
} from "@/components/OrganizationLinkButton";
import OrganizationStatus from "@/components/organizationStatus";
import { EventStatus } from "@/constants/event";
import type { EventItem } from "@/types/event";
import { getEventCoverImgPath, imageUrl } from "@/utils/imageLoader";
import { currentSupportLocale, formatLocale } from "@/utils/locale";
import { eventDescriptionGenerator, keywordGenerator } from "@/utils/meta";
import { generateEventDetailStructuredData } from "@/utils/structuredData";
import { sendTrack } from "@/utils/track";
import { getEventDetailUrl } from "@/utils/url";
import clsx from "clsx";
import { GetServerSidePropsContext } from "next";
import { useTranslation } from "next-i18next/pages";
import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
import Link from "next/link";
import { BsCalendar2DateFill } from "react-icons/bs";
import { FaHotel, FaPeoplePulling } from "react-icons/fa6";
import { IoLocation } from "react-icons/io5";
Expand Down Expand Up @@ -84,7 +71,14 @@ export default function EventDetail({ event }: { event: EventItem }) {
{event.name}
</h2>
<h2 className="text-gray-600 text-sm flex">
{t("event.hostBy", { hostName: event.organization?.name })}
{t("event.hostBy", {
hostName: event.organizations.length
? [
event.organization?.name ?? "",
...event.organizations.map((organization) => organization.name),
].filter(Boolean).join("、")
: event.organization?.name,
})}
Comment thread
PaiJi marked this conversation as resolved.
{/* <EventStatusBar className="ml-2" pageviews="0" fav="2" /> */}
</h2>

Expand Down Expand Up @@ -162,86 +156,7 @@ export default function EventDetail({ event }: { event: EventItem }) {
</div>
)}

<div
id="event-detail__right"
className={clsx(
"bg-white rounded-xl mb-4 lg:mb-0",
!showDescriptionContainer && "w-full",
showDescriptionContainer && "md:w-4/12",
)}
>
<div className="p-4">
<div className="flex">
{event.organization?.logoUrl && (
<div className="border rounded flex justify-center items-center p-2 w-[100px] h-[100px]">
<NextImage
className="object-contain"
alt={`${event.organization?.name}'s logo`}
width={200}
height={200}
src={event.organization.logoUrl}
autoFormat
/>
</div>
)}
<div className="ml-4 flex flex-col justify-between">
<div>
<Link
className="text-2xl font-bold text-gray-600"
target="_blank"
href={`/${event.organization?.slug}`}
>
{event.organization?.name}
</Link>
<div className="flex items-center text-gray-500 mb-4">
<span className="text-sm">
<OrganizationStatus status={event.organization?.status || ""} />
</span>
</div>
</div>

<Link href={`/${event.organization?.slug}`}>
<button
onClick={() =>
sendTrack({
eventName: "click-event-portal",
eventValue: {
link: `/${event.organization?.slug}`,
},
})
}
className="border rounded px-2 py-1 text-sm text-gray-500 hover:border-slate-400 hover:drop-shadow transition duration-200"
>
{t("event.gotoOrganization")}
</button>
</Link>
</div>
</div>

<div
className={clsx(
"items-center text-gray-500 grid gap-4 mt-4",
!showDescriptionContainer && "lg:grid-cols-2",
)}
>
{event.organization?.website && <WebsiteButton t={t} href={event.organization.website} />}
{event.organization?.qqGroup && <QQGroupButton t={t} text={event.organization.qqGroup} />}
{event.organization?.bilibili && <BiliButton t={t} href={event.organization.bilibili} />}

{event.organization?.weibo && <WeiboButton t={t} href={event.organization.weibo} />}

{event.organization?.twitter && <TwitterButton t={t} href={event.organization.twitter} />}

{event.organization?.contactMail && <EmailButton t={t} mail={event.organization.contactMail} />}

{event.organization?.plurk && <PlurkButton t={t} href={event.organization.plurk} />}

{event.organization?.facebook && <FacebookButton t={t} href={event.organization.facebook} />}

{event.organization?.rednote && <RednoteButton t={t} href={event.organization.rednote} />}
</div>
</div>
</div>
<EventOrganizationCard key={event.id} event={event} showDescriptionContainer={showDescriptionContainer} />
</div>
</>
);
Expand Down
1 change: 1 addition & 0 deletions src/types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const EventSchema = z.object({
commonFeatures: z.array(FeatureSchema).nullish(),

organization: OrganizationSchema,
organizations: z.array(OrganizationSchema),
Comment thread
PaiJi marked this conversation as resolved.
});

export type EventItem = z.infer<typeof EventSchema>;
Expand Down
Loading