diff --git a/client/cms/src/assets/event-placeholder.png b/client/cms/src/assets/event-placeholder.png new file mode 100644 index 0000000..dc2c293 Binary files /dev/null and b/client/cms/src/assets/event-placeholder.png differ diff --git a/client/cms/src/client/@tanstack/react-query.gen.ts b/client/cms/src/client/@tanstack/react-query.gen.ts index 731f806..c823dd3 100644 --- a/client/cms/src/client/@tanstack/react-query.gen.ts +++ b/client/cms/src/client/@tanstack/react-query.gen.ts @@ -3,8 +3,8 @@ import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from '../sdk.gen'; -import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse } from '../types.gen'; +import { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from '../sdk.gen'; +import type { GetAuthRedirectData, GetAuthRedirectError, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse, PostEventJoinData, PostEventJoinError, PostEventJoinResponse, PostKycQueryData, PostKycQueryError, PostKycQueryResponse, PostKycSessionData, PostKycSessionError, PostKycSessionResponse } from '../types.gen'; /** * Exchange Auth Code @@ -214,6 +214,25 @@ export const getEventInfoOptions = (options: Options) => query queryKey: getEventInfoQueryKey(options) }); +/** + * Join an Event + * + * Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service. + */ +export const postEventJoinMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postEventJoin({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const getEventListQueryKey = (options: Options) => createQueryKey('getEventList', options); /** @@ -292,6 +311,44 @@ export const getEventListInfiniteOptions = (options: Options) queryKey: getEventListInfiniteQueryKey(options) }); +/** + * Query KYC Status + * + * Checks the current state of a KYC session and updates local database if approved. + */ +export const postKycQueryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postKycQuery({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Create KYC Session + * + * Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI. + */ +export const postKycSessionMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await postKycSession({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const getUserInfoQueryKey = (options?: Options) => createQueryKey('getUserInfo', options); /** diff --git a/client/cms/src/client/index.ts b/client/cms/src/client/index.ts index a9d11b0..a853517 100644 --- a/client/cms/src/client/index.ts +++ b/client/cms/src/client/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit } from './sdk.gen'; -export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen'; +export { getAuthRedirect, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from './sdk.gen'; +export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinError, PostEventJoinErrors, PostEventJoinResponse, PostEventJoinResponses, PostKycQueryData, PostKycQueryError, PostKycQueryErrors, PostKycQueryResponse, PostKycQueryResponses, PostKycSessionData, PostKycSessionError, PostKycSessionErrors, PostKycSessionResponse, PostKycSessionResponses, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventEventJoinData, ServiceKycKycQueryData, ServiceKycKycQueryResponse, ServiceKycKycSessionData, ServiceKycKycSessionResponse, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen'; diff --git a/client/cms/src/client/sdk.gen.ts b/client/cms/src/client/sdk.gen.ts index fd54c66..475743d 100644 --- a/client/cms/src/client/sdk.gen.ts +++ b/client/cms/src/client/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses } from './types.gen'; +import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinErrors, PostEventJoinResponses, PostKycQueryData, PostKycQueryErrors, PostKycQueryResponses, PostKycSessionData, PostKycSessionErrors, PostKycSessionResponses } from './types.gen'; export type Options = Options2 & { /** @@ -116,6 +116,20 @@ export const postEventCheckinSubmit = (opt */ export const getEventInfo = (options: Options) => (options.client ?? client).get({ url: '/event/info', ...options }); +/** + * Join an Event + * + * Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service. + */ +export const postEventJoin = (options: Options) => (options.client ?? client).post({ + url: '/event/join', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + /** * List Events * @@ -123,6 +137,34 @@ export const getEventInfo = (options: Opti */ export const getEventList = (options: Options) => (options.client ?? client).get({ url: '/event/list', ...options }); +/** + * Query KYC Status + * + * Checks the current state of a KYC session and updates local database if approved. + */ +export const postKycQuery = (options: Options) => (options.client ?? client).post({ + url: '/kyc/query', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Create KYC Session + * + * Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI. + */ +export const postKycSession = (options: Options) => (options.client ?? client).post({ + url: '/kyc/session', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + /** * Get My User Information * diff --git a/client/cms/src/client/types.gen.ts b/client/cms/src/client/types.gen.ts index c59d21e..85a22d1 100644 --- a/client/cms/src/client/types.gen.ts +++ b/client/cms/src/client/types.gen.ts @@ -72,6 +72,42 @@ export type ServiceEventCheckinSubmitData = { checkin_code?: string; }; +export type ServiceEventEventJoinData = { + event_id?: string; + kyc_id?: string; +}; + +export type ServiceKycKycQueryData = { + kyc_id?: string; +}; + +export type ServiceKycKycQueryResponse = { + /** + * success | pending | failed + */ + status?: string; +}; + +export type ServiceKycKycSessionData = { + /** + * base64 json + */ + identity?: string; + /** + * cnrid | passport + */ + type?: string; +}; + +export type ServiceKycKycSessionResponse = { + kyc_id?: string; + redirect_uri?: string; + /** + * success | processing + */ + status?: string; +}; + export type ServiceUserUserInfoData = { allow_public?: boolean; avatar?: string; @@ -536,6 +572,66 @@ export type GetEventInfoResponses = { export type GetEventInfoResponse = GetEventInfoResponses[keyof GetEventInfoResponses]; +export type PostEventJoinData = { + /** + * Event Join Details (UserId and EventId are required) + */ + body: ServiceEventEventJoinData; + path?: never; + query?: never; + url: '/event/join'; +}; + +export type PostEventJoinErrors = { + /** + * Invalid Input or UUID Parse Failed + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Missing User ID / Unauthorized + */ + 401: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Unauthorized / Missing User ID + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error / Database Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventJoinError = PostEventJoinErrors[keyof PostEventJoinErrors]; + +export type PostEventJoinResponses = { + /** + * Successfully joined the event + */ + 200: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostEventJoinResponse = PostEventJoinResponses[keyof PostEventJoinResponses]; + export type GetEventListData = { body?: never; path?: never; @@ -592,6 +688,106 @@ export type GetEventListResponses = { export type GetEventListResponse = GetEventListResponses[keyof GetEventListResponses]; +export type PostKycQueryData = { + /** + * KYC query data (KycId) + */ + body: ServiceKycKycQueryData; + path?: never; + query?: never; + url: '/kyc/query'; +}; + +export type PostKycQueryErrors = { + /** + * Invalid UUID or input + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Unauthorized + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostKycQueryError = PostKycQueryErrors[keyof PostKycQueryErrors]; + +export type PostKycQueryResponses = { + /** + * Query processed (success/pending/failed) + */ + 200: UtilsRespStatus & { + data?: ServiceKycKycQueryResponse; + }; +}; + +export type PostKycQueryResponse = PostKycQueryResponses[keyof PostKycQueryResponses]; + +export type PostKycSessionData = { + /** + * KYC session data (Type and Base64 Identity) + */ + body: ServiceKycKycSessionData; + path?: never; + query?: never; + url: '/kyc/session'; +}; + +export type PostKycSessionErrors = { + /** + * Invalid input or decode failed + */ + 400: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Missing User ID + */ + 403: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; + /** + * Internal Server Error / KYC Service Error + */ + 500: UtilsRespStatus & { + data?: { + [key: string]: unknown; + }; + }; +}; + +export type PostKycSessionError = PostKycSessionErrors[keyof PostKycSessionErrors]; + +export type PostKycSessionResponses = { + /** + * Session created successfully + */ + 200: UtilsRespStatus & { + data?: ServiceKycKycSessionResponse; + }; +}; + +export type PostKycSessionResponse = PostKycSessionResponses[keyof PostKycSessionResponses]; + export type GetUserInfoData = { body?: never; path?: never; diff --git a/client/cms/src/client/zod.gen.ts b/client/cms/src/client/zod.gen.ts index 4b87cee..19c081d 100644 --- a/client/cms/src/client/zod.gen.ts +++ b/client/cms/src/client/zod.gen.ts @@ -70,6 +70,30 @@ export const zServiceEventCheckinSubmitData = z.object({ checkin_code: z.optional(z.string()) }); +export const zServiceEventEventJoinData = z.object({ + event_id: z.optional(z.string()), + kyc_id: z.optional(z.string()) +}); + +export const zServiceKycKycQueryData = z.object({ + kyc_id: z.optional(z.string()) +}); + +export const zServiceKycKycQueryResponse = z.object({ + status: z.optional(z.string()) +}); + +export const zServiceKycKycSessionData = z.object({ + identity: z.optional(z.string()), + type: z.optional(z.string()) +}); + +export const zServiceKycKycSessionResponse = z.object({ + kyc_id: z.optional(z.string()), + redirect_uri: z.optional(z.string()), + status: z.optional(z.string()) +}); + export const zServiceUserUserInfoData = z.object({ allow_public: z.optional(z.boolean()), avatar: z.optional(z.string()), @@ -210,6 +234,19 @@ export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({ data: z.optional(zDataEventIndexDoc) })); +export const zPostEventJoinData = z.object({ + body: zServiceEventEventJoinData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successfully joined the event + */ +export const zPostEventJoinResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(z.record(z.string(), z.unknown())) +})); + export const zGetEventListData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), @@ -226,6 +263,32 @@ export const zGetEventListResponse = zUtilsRespStatus.and(z.object({ data: z.optional(z.array(zDataEventIndexDoc)) })); +export const zPostKycQueryData = z.object({ + body: zServiceKycKycQueryData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Query processed (success/pending/failed) + */ +export const zPostKycQueryResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceKycKycQueryResponse) +})); + +export const zPostKycSessionData = z.object({ + body: zServiceKycKycSessionData, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Session created successfully + */ +export const zPostKycSessionResponse = zUtilsRespStatus.and(z.object({ + data: z.optional(zServiceKycKycSessionResponse) +})); + export const zGetUserInfoData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), diff --git a/client/cms/src/components/events/event-card.skeleton.tsx b/client/cms/src/components/events/event-card.skeleton.tsx new file mode 100644 index 0000000..0a874c0 --- /dev/null +++ b/client/cms/src/components/events/event-card.skeleton.tsx @@ -0,0 +1,40 @@ +import { Calendar } from 'lucide-react'; +import { + Card, + CardAction, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Badge } from '../ui/badge'; +import { Skeleton } from '../ui/skeleton'; + +export function EventCardSkeleton() { + return ( + +
+ + + + Official + + + + + + + + + + + + + + + + + ); +} diff --git a/client/cms/src/components/workbenchCards/event-card.view.tsx b/client/cms/src/components/events/event-card.view.tsx similarity index 80% rename from client/cms/src/components/workbenchCards/event-card.view.tsx rename to client/cms/src/components/events/event-card.view.tsx index 7bb21cf..3b5d5fd 100644 --- a/client/cms/src/components/workbenchCards/event-card.view.tsx +++ b/client/cms/src/components/events/event-card.view.tsx @@ -1,3 +1,4 @@ +import type { EventInfo } from './types'; import dayjs from 'dayjs'; import { Calendar } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; @@ -10,16 +11,9 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; +import { Skeleton } from '../ui/skeleton'; -export function EventCardView({ type, coverImage, eventName, description, startTime, endTime }: - { - type: 'official' | 'party'; - coverImage: string; - eventName: string; - description: string; - startTime: Date; - endTime: Date; - }) { +export function EventCardView({ type, coverImage, eventName, description, startTime, endTime, onJoinEvent }: EventInfo) { const startDayJs = dayjs(startTime); const endDayJs = dayjs(endTime); return ( @@ -30,6 +24,9 @@ export function EventCardView({ type, coverImage, eventName, description, startT alt="Event cover" className="relative z-20 aspect-video w-full object-cover rounded-t-xl" /> + {type === 'official' ? Official : Party} @@ -42,10 +39,9 @@ export function EventCardView({ type, coverImage, eventName, description, startT {description} - - + ); diff --git a/client/cms/src/components/events/event-grid.container.tsx b/client/cms/src/components/events/event-grid.container.tsx new file mode 100644 index 0000000..e0e981f --- /dev/null +++ b/client/cms/src/components/events/event-grid.container.tsx @@ -0,0 +1,23 @@ +import type { EventInfo } from './types'; +import PlaceholderImage from '@/assets/event-placeholder.png'; +import { useGetEvents } from '@/hooks/data/useGetEvents'; +import { useJoinEvent } from '@/hooks/data/useJoinEvent'; +import { EventGridView } from './event-grid.view'; + +export function EventGridContainer() { + const { data, isLoading } = useGetEvents(); + const { mutate } = useJoinEvent(); + const allEvents: EventInfo[] = isLoading + ? [] + : data.pages.flatMap(page => page.data!).map(it => ({ + type: it.type! as EventInfo['type'], + coverImage: it.thumbnail! || PlaceholderImage, + eventName: it.name!, + description: it.description!, + startTime: new Date(it.start_time!), + endTime: new Date(it.end_time!), + onJoinEvent: () => mutate({ body: { event_id: it.event_id } }), + } satisfies EventInfo)); + + return ; +} diff --git a/client/cms/src/components/events/event-grid.skeleton.tsx b/client/cms/src/components/events/event-grid.skeleton.tsx new file mode 100644 index 0000000..7c0046a --- /dev/null +++ b/client/cms/src/components/events/event-grid.skeleton.tsx @@ -0,0 +1,12 @@ +import { EventCardSkeleton } from './event-card.skeleton'; + +export function EventGridSkeleton() { + return ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} +
+ ); +} diff --git a/client/cms/src/components/events/event-grid.view.tsx b/client/cms/src/components/events/event-grid.view.tsx new file mode 100644 index 0000000..79c90c1 --- /dev/null +++ b/client/cms/src/components/events/event-grid.view.tsx @@ -0,0 +1,12 @@ +import type { EventInfo } from './types'; +import { EventCardView } from './event-card.view'; + +export function EventGridView({ events }: { events: EventInfo[] }) { + return ( +
+ {events.map(event => ( + + ))} +
+ ); +} diff --git a/client/cms/src/components/events/types.ts b/client/cms/src/components/events/types.ts new file mode 100644 index 0000000..22f52de --- /dev/null +++ b/client/cms/src/components/events/types.ts @@ -0,0 +1,9 @@ +export interface EventInfo { + type: 'official' | 'party'; + coverImage: string; + eventName: string; + description: string; + startTime: Date; + endTime: Date; + onJoinEvent: () => void; +} diff --git a/client/cms/src/components/workbenchCards/card-skeleton.tsx b/client/cms/src/components/workbenchCards/card-skeleton.tsx deleted file mode 100644 index 007b499..0000000 --- a/client/cms/src/components/workbenchCards/card-skeleton.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Skeleton } from '../ui/skeleton'; - -export function CardSkeleton() { - return ( - - ); -} diff --git a/client/cms/src/hooks/data/useGetEvents.ts b/client/cms/src/hooks/data/useGetEvents.ts new file mode 100644 index 0000000..4e02244 --- /dev/null +++ b/client/cms/src/hooks/data/useGetEvents.ts @@ -0,0 +1,21 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { isNil } from 'lodash-es'; +import { getEventListInfiniteOptions } from '@/client/@tanstack/react-query.gen'; + +const LIMIT = 12; + +export function useGetEvents() { + return useInfiniteQuery({ + ...getEventListInfiniteOptions({ + query: { limit: String(LIMIT), offset: String(0) }, + }), + initialPageParam: '0', + getNextPageParam: (lastPage, allPages) => { + const currentData = lastPage?.data; + if (!isNil(currentData) && currentData.length === LIMIT) { + return String(allPages.length * LIMIT); + } + return undefined; + }, + }); +} diff --git a/client/cms/src/hooks/data/useJoinEvent.ts b/client/cms/src/hooks/data/useJoinEvent.ts new file mode 100644 index 0000000..ae273ac --- /dev/null +++ b/client/cms/src/hooks/data/useJoinEvent.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { postEventJoinMutation } from '@/client/@tanstack/react-query.gen'; + +export function useJoinEvent() { + return useMutation({ + ...postEventJoinMutation(), + }); +} diff --git a/client/cms/src/routes/_workbenchLayout/events.tsx b/client/cms/src/routes/_workbenchLayout/events.tsx index 22bd0b1..79e31d1 100644 --- a/client/cms/src/routes/_workbenchLayout/events.tsx +++ b/client/cms/src/routes/_workbenchLayout/events.tsx @@ -1,9 +1,14 @@ import { createFileRoute } from '@tanstack/react-router'; +import { EventGridContainer } from '@/components/events/event-grid.container'; export const Route = createFileRoute('/_workbenchLayout/events')({ component: RouteComponent, }); function RouteComponent() { - return
Hello "/_sidebarLayout/events"!
; + return ( +
+ +
+ ); } diff --git a/client/cms/src/stories/event-card.stories.ts b/client/cms/src/stories/event-card.stories.ts deleted file mode 100644 index 6a6017b..0000000 --- a/client/cms/src/stories/event-card.stories.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { EventCardView } from '@/components/workbenchCards/event-card.view'; - -const meta = { - title: 'Cards/EventCard', - component: EventCardView, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - args: { - type: 'official', - coverImage: "https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true", - eventName: 'Nix CN Conference 26.05', - description: 'Event Description', - startTime: "2026-06-13T04:00:00.000Z", - endTime: "2026-06-14T04:00:00.000Z", - }, -}; diff --git a/client/cms/src/stories/events/event-card.stories.tsx b/client/cms/src/stories/events/event-card.stories.tsx new file mode 100644 index 0000000..9180224 --- /dev/null +++ b/client/cms/src/stories/events/event-card.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { EventCardSkeleton } from '@/components/events/event-card.skeleton'; +import { EventCardView } from '@/components/events/event-card.view'; + +const meta = { + title: 'Events/EventCard', + component: EventCardView, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + onJoinEvent: () => { }, + }, +}; + +export const Loading: Story = { + render: () => , + args: { + type: 'official', + coverImage: '', + eventName: '', + description: '', + startTime: new Date(0), + endTime: new Date(0), + onJoinEvent: () => { }, + }, +}; diff --git a/client/cms/src/stories/events/event-grid.stories.tsx b/client/cms/src/stories/events/event-grid.stories.tsx new file mode 100644 index 0000000..bb63feb --- /dev/null +++ b/client/cms/src/stories/events/event-grid.stories.tsx @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { EventGridSkeleton } from '@/components/events/event-grid.skeleton'; +import { EventGridView } from '@/components/events/event-grid.view'; + +const meta = { + title: 'Events/EventGrid', + component: EventGridView, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + events: [ + { + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-watersplash.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + onJoinEvent: () => { }, + }, + { + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-moonscape.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + onJoinEvent: () => { }, + }, + { + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nix-wallpaper-nineish-catppuccin-latte.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + onJoinEvent: () => { }, + }, + { + type: 'official', + coverImage: 'https://github.com/NixOS/nixos-artwork/blob/master/wallpapers/nixos-wallpaper-catppuccin-macchiato.png?raw=true', + eventName: 'Nix CN Conference 26.05', + description: 'Event Description', + startTime: new Date('2026-06-13T04:00:00.000Z'), + endTime: new Date('2026-06-14T04:00:00.000Z'), + onJoinEvent: () => { }, + }, + ], + }, +}; + +export const Skeleton: Story = { + render: () => , + args: { + events: [], + }, +}; diff --git a/client/cms/src/stories/sidebar.stories.tsx b/client/cms/src/stories/layout/sidebar.stories.tsx similarity index 93% rename from client/cms/src/stories/sidebar.stories.tsx rename to client/cms/src/stories/layout/sidebar.stories.tsx index 0cab568..ac2aeac 100644 --- a/client/cms/src/stories/sidebar.stories.tsx +++ b/client/cms/src/stories/layout/sidebar.stories.tsx @@ -4,10 +4,10 @@ import { NavUserSkeleton } from '@/components/sidebar/nav-user.skeletion'; import { NavUserView } from '@/components/sidebar/nav-user.view'; import { SidebarProvider } from '@/components/ui/sidebar'; import { navData } from '@/lib/navData'; -import { user } from './exampleUser'; +import { user } from '../exampleUser'; const meta = { - title: 'Navigation/Sidebar', + title: 'Layout/Sidebar', component: AppSidebar, decorators: [ Story => (