Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"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.dateFormat": "MMMM DD, YYYY",
"organization.active": "Active Organizers",
"organization.inactive": "Inactive Organizers",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"event.mapLoading": "Открыть в Gaode Map...",
"event.gotoGaoDe": "Открыть в Gaode Map",
"event.gotoOrganization": "Участники и стенды",
"event.hostSwitchGroupAria": "Переключить организатора",
"event.prevHostOrganization": "Предыдущий организатор",
"event.nextHostOrganization": "Следующий организатор",
"event.dateFormat": "MMMM DD, YYYY",
"organization.active": "Активные организаторы",
"organization.inactive": "Неактивные организаторы",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/zh-Hans/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
"event.retryLoadMap": "尝试重新加载地图",
"event.gotoGaoDe": "去高德地图查看",
"event.gotoOrganization": "看看展商详情",
"event.hostSwitchGroupAria": "切换主办/展商",
"event.prevHostOrganization": "上一个主办",
"event.nextHostOrganization": "下一个主办",
"event.dateFormat": "YYYY年MM月DD日",
"organization.active": "活跃展商",
"organization.inactive": "停止活动展商",
Expand Down
3 changes: 3 additions & 0 deletions public/locales/zh-Hant/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
"event.retryLoadMap": "重新載入地圖",
"event.gotoGaoDe": "用高德地圖查看",
"event.gotoOrganization": "查看主辦單位",
"event.hostSwitchGroupAria": "切換主辦/展商",
"event.prevHostOrganization": "上一個主辦",
"event.nextHostOrganization": "下一個主辦",
"event.dateFormat": "YYYY年MM月DD日",
"organization.active": "營運中",
"organization.inactive": "已停辦",
Expand Down
147 changes: 147 additions & 0 deletions src/components/event/EventOrganizationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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 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">主办方 {activeIndex + 1}/{orgList.length}</span>
Comment thread
PaiJi marked this conversation as resolved.
Outdated
<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={`${organization.name}'s logo`}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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" target="_blank" href={`/${organization.slug}`}>
{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={`/${organization.slug}`}>
<button
onClick={() =>
sendTrack({
eventName: "click-event-portal",
eventValue: {
link: `/${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>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</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>
);
}
102 changes: 7 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,11 @@ 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).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 +153,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