Skip to content

Commit 0d85473

Browse files
committed
feat: individual ama page
1 parent af0c76e commit 0d85473

File tree

17 files changed

+592
-13
lines changed

17 files changed

+592
-13
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
'use client';
2+
3+
import type { GuildChannelInfo, PossiblyMissingChannelInfo } from '@chatsift/api';
4+
import { useParams, useRouter } from 'next/navigation';
5+
import { useState } from 'react';
6+
import { Button } from '@/components/common/Button';
7+
import { Skeleton } from '@/components/common/Skeleton';
8+
import { SvgChannelText } from '@/components/icons/channels/SvgChannelText';
9+
import { client } from '@/data/client';
10+
import { getChannelIcon } from '@/utils/channels';
11+
import { formatDate } from '@/utils/util';
12+
13+
const channelName = (channel: GuildChannelInfo | PossiblyMissingChannelInfo | null) =>
14+
channel && 'name' in channel ? channel.name : 'Unknown';
15+
16+
function ChannelIcon({ channel }: { readonly channel: GuildChannelInfo | PossiblyMissingChannelInfo | null }) {
17+
if (!channel || !('type' in channel)) return null;
18+
const Icon = getChannelIcon(channel.type);
19+
return <Icon size={20} />;
20+
}
21+
22+
export function AMADetails() {
23+
const params = useParams<{ amaId: string; id: string }>();
24+
const router = useRouter();
25+
const [isEndingAMA, setIsEndingAMA] = useState(false);
26+
const [showEndConfirm, setShowEndConfirm] = useState(false);
27+
28+
const { data: ama, isLoading } = client.guilds.ama.useAMA(params.id, params.amaId);
29+
const updateAMA = client.guilds.ama.useUpdateAMA(params.id, params.amaId);
30+
const repostPrompt = client.guilds.ama.useRepostPrompt(params.id, params.amaId);
31+
32+
if (isLoading) {
33+
return (
34+
<div className="space-y-6">
35+
<Skeleton className="h-64 w-full" />
36+
</div>
37+
);
38+
}
39+
40+
if (!ama) {
41+
return (
42+
<div className="text-center py-12">
43+
<p className="text-xl text-secondary dark:text-secondary-dark">AMA session not found</p>
44+
</div>
45+
);
46+
}
47+
48+
const handleEndAMA = async () => {
49+
if (!showEndConfirm) {
50+
setShowEndConfirm(true);
51+
return;
52+
}
53+
54+
try {
55+
setIsEndingAMA(true);
56+
await updateAMA.mutateAsync({ ended: true });
57+
router.push(`/dashboard/${params.id}/ama/amas`);
58+
} catch (error) {
59+
console.error('Failed to end AMA:', error);
60+
} finally {
61+
setIsEndingAMA(false);
62+
setShowEndConfirm(false);
63+
}
64+
};
65+
66+
const handleRepostPrompt = async () => {
67+
try {
68+
await repostPrompt.mutateAsync({});
69+
} catch (error) {
70+
console.error('Failed to repost prompt:', error);
71+
}
72+
};
73+
74+
return (
75+
<div className="space-y-6">
76+
<div className="rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark">
77+
<h2 className="text-xl font-medium text-primary dark:text-primary-dark mb-4">Session Information</h2>
78+
<div className="space-y-4">
79+
<div>
80+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark mb-1">Title</p>
81+
<p className="text-lg text-primary dark:text-primary-dark">{ama.title}</p>
82+
</div>
83+
84+
<div>
85+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark mb-1">Status</p>
86+
<span
87+
className={`inline-block rounded px-3 py-1 text-sm font-medium ${
88+
ama.ended ? 'bg-misc-danger/10 text-misc-danger' : 'bg-misc-accent/10 text-misc-accent'
89+
}`}
90+
>
91+
{ama.ended ? 'Ended' : 'Active'}
92+
</span>
93+
</div>
94+
95+
<div>
96+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark mb-1">Questions</p>
97+
<p className="text-lg text-primary dark:text-primary-dark">
98+
{ama.questionCount} {ama.questionCount === 1 ? 'question' : 'questions'}
99+
</p>
100+
</div>
101+
102+
<div>
103+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark mb-1">Created</p>
104+
<p className="text-lg text-primary dark:text-primary-dark">{formatDate(new Date(ama.createdAt))}</p>
105+
</div>
106+
</div>
107+
</div>
108+
109+
{/* Channels Card */}
110+
<div className="rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark">
111+
<h2 className="text-xl font-medium text-primary dark:text-primary-dark mb-4">Channels</h2>
112+
<div className="space-y-4">
113+
<div className="flex items-center gap-3">
114+
<ChannelIcon channel={ama.answersChannel} />
115+
<div>
116+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark">Answers Channel</p>
117+
<p className="text-base text-primary dark:text-primary-dark">{channelName(ama.answersChannel)}</p>
118+
</div>
119+
</div>
120+
121+
<div className="flex items-center gap-3">
122+
<ChannelIcon channel={ama.promptChannel} />
123+
<div>
124+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark">Prompt Channel</p>
125+
<p className="text-base text-primary dark:text-primary-dark">{channelName(ama.promptChannel)}</p>
126+
</div>
127+
</div>
128+
129+
{ama.modQueueChannel && (
130+
<div className="flex items-center gap-3">
131+
<ChannelIcon channel={ama.modQueueChannel} />
132+
<div>
133+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark">Mod Queue</p>
134+
<p className="text-base text-primary dark:text-primary-dark">{channelName(ama.modQueueChannel)}</p>
135+
</div>
136+
</div>
137+
)}
138+
139+
{ama.flaggedQueueChannel && (
140+
<div className="flex items-center gap-3">
141+
<ChannelIcon channel={ama.flaggedQueueChannel} />
142+
<div>
143+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark">Flagged Queue</p>
144+
<p className="text-base text-primary dark:text-primary-dark">{channelName(ama.flaggedQueueChannel)}</p>
145+
</div>
146+
</div>
147+
)}
148+
149+
{ama.guestQueueChannel && (
150+
<div className="flex items-center gap-3">
151+
<ChannelIcon channel={ama.guestQueueChannel} />
152+
<div>
153+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark">Guest Queue</p>
154+
<p className="text-base text-primary dark:text-primary-dark">{channelName(ama.guestQueueChannel)}</p>
155+
</div>
156+
</div>
157+
)}
158+
</div>
159+
</div>
160+
161+
<div className="rounded-lg border border-on-secondary bg-card p-6 dark:border-on-secondary-dark dark:bg-card-dark">
162+
<h2 className="text-xl font-medium text-primary dark:text-primary-dark mb-4">Prompt Message Status</h2>
163+
<div className="space-y-4">
164+
<div>
165+
<p className="text-sm font-medium text-secondary dark:text-secondary-dark mb-1">Message Status</p>
166+
<span
167+
className={`inline-block rounded px-3 py-1 text-sm font-medium ${
168+
ama.promptMessageExists ? 'bg-misc-accent/10 text-misc-accent' : 'bg-misc-danger/10 text-misc-danger'
169+
}`}
170+
>
171+
{ama.promptMessageExists ? 'Active on Discord' : 'Message Deleted'}
172+
</span>
173+
</div>
174+
175+
{!ama.promptMessageExists && !ama.ended && (
176+
<div className="pt-2">
177+
<Button
178+
className="px-6 py-3 bg-misc-accent text-white rounded-md hover:bg-misc-accent/90 transition-colors disabled:opacity-50"
179+
onPress={handleRepostPrompt}
180+
type="button"
181+
>
182+
Repost Prompt Message
183+
</Button>
184+
<p className="mt-2 text-sm text-secondary dark:text-secondary-dark">
185+
This will create a new prompt message in the prompt channel.
186+
</p>
187+
</div>
188+
)}
189+
</div>
190+
</div>
191+
192+
{/* Actions Card */}
193+
{!ama.ended && (
194+
<div className="rounded-lg border border-misc-danger/20 bg-card p-6 dark:border-misc-danger/20 dark:bg-card-dark">
195+
<h2 className="text-xl font-medium text-misc-danger mb-4">Danger Zone</h2>
196+
{showEndConfirm ? (
197+
<div className="space-y-4">
198+
<p className="text-base text-primary dark:text-primary-dark">
199+
Are you sure you want to end this AMA? This action is <strong>irreversible</strong>.
200+
</p>
201+
<div className="flex gap-3">
202+
<Button
203+
className="px-6 py-3 bg-misc-danger text-white rounded-md hover:bg-misc-danger/90 transition-colors disabled:opacity-50"
204+
isDisabled={isEndingAMA}
205+
onPress={handleEndAMA}
206+
type="button"
207+
>
208+
{isEndingAMA ? 'Ending...' : 'Yes, End AMA'}
209+
</Button>
210+
<Button
211+
className="px-6 py-3 bg-on-tertiary dark:bg-on-tertiary-dark text-primary dark:text-primary-dark rounded-md hover:bg-on-secondary dark:hover:bg-on-secondary-dark transition-colors"
212+
onPress={() => setShowEndConfirm(false)}
213+
type="button"
214+
>
215+
Cancel
216+
</Button>
217+
</div>
218+
</div>
219+
) : (
220+
<div className="space-y-4">
221+
<p className="text-base text-primary dark:text-primary-dark">
222+
Ending an AMA will prevent new questions from being submitted. This action cannot be undone.
223+
</p>
224+
<Button
225+
className="px-6 py-3 bg-misc-danger text-white rounded-md hover:bg-misc-danger/90 transition-colors"
226+
onPress={handleEndAMA}
227+
type="button"
228+
>
229+
End AMA Session
230+
</Button>
231+
</div>
232+
)}
233+
</div>
234+
)}
235+
</div>
236+
);
237+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { AMADashboardCrumbs } from '../../_components/AMADashboardCrumbs';
2+
import { AMADetails } from './_components/AMADetails';
3+
import { Heading } from '@/components/common/Heading';
4+
5+
export default function AMADetailPage() {
6+
return (
7+
<div className="flex flex-col [&:not]:first-of-type:mt-8 [&>*]:first-of-type:mb-4">
8+
<AMADashboardCrumbs />
9+
<Heading subtitle="View and manage this AMA session" title="AMA Session Details" />
10+
<AMADetails />
11+
</div>
12+
);
13+
}

apps/website/src/data/client.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,23 @@ export const client = {
100100
useCreateAMA: (guildId: string) =>
101101
useMutateIt(routesInfo.guilds(guildId).ama.amas(), 'POST', async (queryClient) => {
102102
await queryClient.invalidateQueries({
103-
queryKey: [
104-
routesInfo.guilds(guildId).ama.amas({ include_ended: 'false' }).queryKey,
105-
routesInfo.guilds(guildId).ama.amas({ include_ended: 'true' }).queryKey,
106-
],
103+
queryKey: ['guilds', guildId, 'ama', 'amas'],
107104
});
108105
}),
109106
useAMAs: (guildId: string, query: GetAMAsQuery) => useQueryIt(routesInfo.guilds(guildId).ama.amas(query)),
107+
useAMA: (guildId: string, amaId: string) => useQueryIt(routesInfo.guilds(guildId).ama.ama(amaId)),
108+
useUpdateAMA: (guildId: string, amaId: string) =>
109+
useMutateIt(routesInfo.guilds(guildId).ama.updateAMA(amaId), 'PATCH', async (queryClient) => {
110+
await queryClient.invalidateQueries({
111+
queryKey: ['guilds', guildId, 'ama', 'amas'],
112+
});
113+
}),
114+
useRepostPrompt: (guildId: string, amaId: string) =>
115+
useMutateIt(routesInfo.guilds(guildId).ama.repostPrompt(amaId), 'POST', async (queryClient) => {
116+
await queryClient.invalidateQueries({
117+
queryKey: routesInfo.guilds(guildId).ama.ama(amaId).queryKey,
118+
});
119+
}),
110120
},
111121
},
112122
} as const;

apps/website/src/data/common.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ export const routesInfo = {
6363
query: { include_ended: query?.include_ended ?? 'false' },
6464
params: { guildId },
6565
}),
66+
67+
ama: (amaId: string) => ({
68+
queryKey: ['guilds', guildId, 'ama', 'ama', amaId],
69+
path: '/v3/guilds/:guildId/ama/amas/:amaId',
70+
query: {},
71+
params: { guildId, amaId },
72+
}),
73+
74+
updateAMA: (amaId: string) => ({
75+
queryKey: ['guilds', guildId, 'ama', 'amas', amaId, 'update'],
76+
path: '/v3/guilds/:guildId/ama/amas/:amaId',
77+
query: {},
78+
params: { guildId, amaId },
79+
}),
80+
81+
repostPrompt: (amaId: string) => ({
82+
queryKey: ['guilds', guildId, 'ama', 'amas', amaId, 'prompt'],
83+
path: '/v3/guilds/:guildId/ama/amas/:amaId/prompt',
84+
query: {},
85+
params: { guildId, amaId },
86+
}),
6687
},
6788
}),
6889
} as const satisfies Info;

apps/website/src/utils/util.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,12 @@ export const getGuildAcronym = (guildName: string) =>
2525
.replaceAll("'s ", ' ')
2626
.replaceAll(/\w+/g, (substring) => substring[0]!)
2727
.replaceAll(/\s/g, '');
28+
29+
export const formatDate = (date: Date) =>
30+
new Intl.DateTimeFormat('en-US', {
31+
year: 'numeric',
32+
month: 'long',
33+
day: 'numeric',
34+
hour: '2-digit',
35+
minute: '2-digit',
36+
}).format(date);
File renamed without changes.

services/api/src/routes/_types/routeTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export type * from '../ama/createAMA.js';
2+
export type * from '../ama/getAMA.js';
23
export type * from '../ama/getAMAs.js';
4+
export type * from '../ama/updateAMA.js';
5+
export type * from '../ama/repostPrompt.js';
36

47
export type * from '../auth/discord.js';
58
export type * from '../auth/discordCallback.js';

services/api/src/routes/ama/createAMA.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export default class CreateAMA extends Route<CreateAMAResult, typeof bodySchema>
129129
promptMessageId: promptMessage.id,
130130
promptJSONData: JSON.stringify(messageBodyBase),
131131
})
132-
.executeTakeFirstOrThrow();
132+
.execute();
133133

134134
return session;
135135
});

0 commit comments

Comments
 (0)