cms: events and kyc #10

Merged
sugar merged 12 commits from noa.virellia/events-list into develop 2026-02-05 11:22:53 +00:00
20 changed files with 600 additions and 49 deletions
Showing only changes of commit 094d02d203 - Show all commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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<GetEventInfoData>) => 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<Options<PostEventJoinData>>): UseMutationOptions<PostEventJoinResponse, PostEventJoinError, Options<PostEventJoinData>> => {
const mutationOptions: UseMutationOptions<PostEventJoinResponse, PostEventJoinError, Options<PostEventJoinData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postEventJoin({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getEventListQueryKey = (options: Options<GetEventListData>) => createQueryKey('getEventList', options);
/**
@@ -292,6 +311,44 @@ export const getEventListInfiniteOptions = (options: Options<GetEventListData>)
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<Options<PostKycQueryData>>): UseMutationOptions<PostKycQueryResponse, PostKycQueryError, Options<PostKycQueryData>> => {
const mutationOptions: UseMutationOptions<PostKycQueryResponse, PostKycQueryError, Options<PostKycQueryData>> = {
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<Options<PostKycSessionData>>): UseMutationOptions<PostKycSessionResponse, PostKycSessionError, Options<PostKycSessionData>> => {
const mutationOptions: UseMutationOptions<PostKycSessionResponse, PostKycSessionError, Options<PostKycSessionData>> = {
mutationFn: async (fnOptions) => {
const { data } = await postKycSession({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getUserInfoQueryKey = (options?: Options<GetUserInfoData>) => createQueryKey('getUserInfo', options);
/**

View File

@@ -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';

View File

@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -116,6 +116,20 @@ export const postEventCheckinSubmit = <ThrowOnError extends boolean = false>(opt
*/
export const getEventInfo = <ThrowOnError extends boolean = false>(options: Options<GetEventInfoData, ThrowOnError>) => (options.client ?? client).get<GetEventInfoResponses, GetEventInfoErrors, ThrowOnError>({ 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 = <ThrowOnError extends boolean = false>(options: Options<PostEventJoinData, ThrowOnError>) => (options.client ?? client).post<PostEventJoinResponses, PostEventJoinErrors, ThrowOnError>({
url: '/event/join',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* List Events
*
@@ -123,6 +137,34 @@ export const getEventInfo = <ThrowOnError extends boolean = false>(options: Opti
*/
export const getEventList = <ThrowOnError extends boolean = false>(options: Options<GetEventListData, ThrowOnError>) => (options.client ?? client).get<GetEventListResponses, GetEventListErrors, ThrowOnError>({ url: '/event/list', ...options });
/**
* Query KYC Status
*
* Checks the current state of a KYC session and updates local database if approved.
*/
export const postKycQuery = <ThrowOnError extends boolean = false>(options: Options<PostKycQueryData, ThrowOnError>) => (options.client ?? client).post<PostKycQueryResponses, PostKycQueryErrors, ThrowOnError>({
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 = <ThrowOnError extends boolean = false>(options: Options<PostKycSessionData, ThrowOnError>) => (options.client ?? client).post<PostKycSessionResponses, PostKycSessionErrors, ThrowOnError>({
url: '/kyc/session',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Get My User Information
*

View File

@@ -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;

View File

@@ -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()),

View File

@@ -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 (
<Card className="relative mx-auto w-full max-w-sm pt-0">
<div className="absolute inset-0 z-30 aspect-video bg-black/10" />
<Skeleton
className="relative z-20 aspect-video w-full object-cover rounded-t-xl"
/>
<CardHeader>
<CardAction>
<Badge variant="secondary" className="bg-accent animate-pulse text-accent select-none">Official</Badge>
</CardAction>
<CardTitle>
<Skeleton className="h-4 max-w-48" />
</CardTitle>
<CardDescription className="flex flex-row items-center text-xs">
<Calendar className="size-4 mr-2" />
<Skeleton className="h-4 w-24" />
</CardDescription>
<CardDescription className="mt-1">
<Skeleton className="h-5 max-w-64" />
</CardDescription>
</CardHeader>
<CardFooter>
<Skeleton className="h-9 px-4 py-2 w-full"></Skeleton>
</CardFooter>
</Card>
);
}

View File

@@ -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"
/>
<Skeleton
className="absolute z-15 aspect-video w-full object-cover rounded-t-xl"
/>
<CardHeader>
<CardAction>
{type === 'official' ? <Badge variant="secondary">Official</Badge> : <Badge variant="destructive">Party</Badge>}
@@ -42,10 +39,9 @@ export function EventCardView({ type, coverImage, eventName, description, startT
<CardDescription className="mt-1">
{description}
</CardDescription>
</CardHeader>
<CardFooter>
<Button className="w-full"></Button>
<Button className="w-full" onClick={onJoinEvent}></Button>
</CardFooter>
</Card>
);

View File

@@ -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 <EventGridView events={allEvents} />;
}

View File

@@ -0,0 +1,12 @@
import { EventCardSkeleton } from './event-card.skeleton';
export function EventGridSkeleton() {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<EventCardSkeleton key={i} />
))}
</div>
);
}

View File

@@ -0,0 +1,12 @@
import type { EventInfo } from './types';
import { EventCardView } from './event-card.view';
export function EventGridView({ events }: { events: EventInfo[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{events.map(event => (
<EventCardView key={event.eventName} {...event} />
))}
</div>
);
}

View File

@@ -0,0 +1,9 @@
export interface EventInfo {
type: 'official' | 'party';
coverImage: string;
eventName: string;
description: string;
startTime: Date;
endTime: Date;
onJoinEvent: () => void;
}

View File

@@ -1,9 +0,0 @@
import { Skeleton } from '../ui/skeleton';
export function CardSkeleton() {
return (
<Skeleton
className="gap-6 rounded-xl py-6 h-full"
/>
);
}

View File

@@ -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;
},
});
}

View File

@@ -0,0 +1,8 @@
import { useMutation } from '@tanstack/react-query';
import { postEventJoinMutation } from '@/client/@tanstack/react-query.gen';
export function useJoinEvent() {
return useMutation({
...postEventJoinMutation(),
});
}

View File

@@ -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 <div>Hello "/_sidebarLayout/events"!</div>;
return (
<div className="py-4 px-6 md:gap-6 md:py-6">
<EventGridContainer />
</div>
);
}

View File

@@ -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<typeof EventCardView>;
export default meta;
type Story = StoryObj<typeof meta>;
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",
},
};

View File

@@ -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<typeof EventCardView>;
export default meta;
type Story = StoryObj<typeof meta>;
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: () => <EventCardSkeleton />,
args: {
type: 'official',
coverImage: '',
eventName: '',
description: '',
startTime: new Date(0),
endTime: new Date(0),
onJoinEvent: () => { },
},
};

View File

@@ -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<typeof EventGridView>;
export default meta;
type Story = StoryObj<typeof meta>;
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: () => <EventGridSkeleton />,
args: {
events: [],
},
};

View File

@@ -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 => (