Skip to content

Commit 5027a7a

Browse files
committed
feat (add where poll): finish add where poll conditionally, integrate backend to basic features
1 parent 60d90bd commit 5027a7a

File tree

10 files changed

+9930
-9172
lines changed

10 files changed

+9930
-9172
lines changed

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/page.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ export default async function MeetTogetherPlanDetailsPage({ params }: Props) {
1919
const platformUser = await getCurrentUser(true);
2020
const plan = await getPlan(planId);
2121
const users = await getUsers(planId);
22+
const polls = await getPollsForPlan(planId);
2223
const timeblocks = await getTimeBlocks(planId);
24+
const isCreator = Boolean(
25+
platformUser?.id && plan?.creator_id && platformUser.id === plan.creator_id
26+
);
2327

2428
return (
2529
<div className="flex min-h-screen w-full flex-col items-center">
@@ -34,6 +38,8 @@ export default async function MeetTogetherPlanDetailsPage({ params }: Props) {
3438
>
3539
<PlanDetailsClient
3640
plan={plan}
41+
isCreator={isCreator}
42+
polls={polls}
3743
platformUser={platformUser}
3844
users={users}
3945
timeblocks={timeblocks}
@@ -95,3 +101,105 @@ async function getTimeBlocks(planId: string) {
95101
})),
96102
];
97103
}
104+
export async function getPollsForPlan(planId: string) {
105+
const sbAdmin = await createAdminClient();
106+
107+
// 1. Get all polls for the plan
108+
const { data: polls, error: pollsError } = await sbAdmin
109+
.from('polls')
110+
.select(
111+
'id, name, plan_id, created_at, creator_id, allow_anonymous_updates'
112+
)
113+
.eq('plan_id', planId);
114+
115+
if (pollsError) {
116+
console.log('Error fetching polls');
117+
return {
118+
error: 'Error fetching polls',
119+
polls: null,
120+
userVotes: [],
121+
guestVotes: [],
122+
};
123+
}
124+
125+
// 2. Get options for all polls
126+
const pollIds = polls?.map((p) => p.id) ?? [];
127+
let allOptions: any[] = [];
128+
if (pollIds.length > 0) {
129+
const { data: options, error: optionsError } = await sbAdmin
130+
.from('poll_options')
131+
.select('id, poll_id, value, created_at')
132+
.in('poll_id', pollIds);
133+
134+
if (optionsError) {
135+
console.log('Error fetching poll options');
136+
return {
137+
error: 'Error fetching poll options',
138+
polls: null,
139+
userVotes: [],
140+
guestVotes: [],
141+
};
142+
}
143+
allOptions = options ?? [];
144+
}
145+
146+
// 3. Get user and guest votes for all poll options
147+
const optionIds = allOptions.map((o) => o.id);
148+
let userVotes: any[] = [];
149+
let guestVotes: any[] = [];
150+
if (optionIds.length > 0) {
151+
// Platform user votes
152+
const { data: uVotes, error: uVotesError } = await sbAdmin
153+
.from('poll_user_votes')
154+
.select('id, option_id, user_id, created_at')
155+
.in('option_id', optionIds);
156+
157+
// Guest votes
158+
const { data: gVotes, error: gVotesError } = await sbAdmin
159+
.from('poll_guest_votes')
160+
.select('id, option_id, guest_id, created_at')
161+
.in('option_id', optionIds);
162+
163+
if (uVotesError || gVotesError) {
164+
console.log('Error fetching votes');
165+
return {
166+
error: 'Error fetching votes',
167+
polls: null,
168+
userVotes: [],
169+
guestVotes: [],
170+
};
171+
}
172+
userVotes = uVotes ?? [];
173+
guestVotes = gVotes ?? [];
174+
}
175+
176+
// 4. Attach options and their votes to polls
177+
const pollsWithOptions = polls.map((poll) => {
178+
const options = allOptions
179+
.filter((opt) => opt.poll_id === poll.id)
180+
.map((opt) => {
181+
const optionUserVotes = userVotes.filter((v) => v.option_id === opt.id);
182+
const optionGuestVotes = guestVotes.filter(
183+
(v) => v.option_id === opt.id
184+
);
185+
186+
return {
187+
...opt,
188+
userVotes: optionUserVotes,
189+
guestVotes: optionGuestVotes,
190+
totalVotes: optionUserVotes.length + optionGuestVotes.length,
191+
};
192+
});
193+
194+
return {
195+
...poll,
196+
options,
197+
};
198+
});
199+
200+
return {
201+
polls: pollsWithOptions,
202+
userVotes,
203+
guestVotes,
204+
};
205+
}

apps/web/src/app/[locale]/(marketing)/meet-together/plans/[planId]/plan-details-client.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import EditPlanDialog from './edit-plan-dialog';
55
import PlanLogin from './plan-login';
66
import PlanUserFilter from './plan-user-filter';
77
import UtilityButtons from './utility-buttons';
8-
import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
8+
import PlanDetailsPolls from '@/app/[locale]/(marketing)/meet-together/plans/[planId]/plan-details-polls';
9+
import type {
10+
GetPollsForPlanResult,
11+
MeetTogetherPlan,
12+
} from '@tuturuuu/types/primitives/MeetTogetherPlan';
913
import type { User } from '@tuturuuu/types/primitives/User';
1014
import { Separator } from '@tuturuuu/ui/separator';
1115
import html2canvas from 'html2canvas-pro';
@@ -14,7 +18,9 @@ import { useCallback } from 'react';
1418

1519
interface PlanDetailsClientProps {
1620
plan: MeetTogetherPlan;
21+
polls: GetPollsForPlanResult | null;
1722
platformUser: User | null;
23+
isCreator: boolean;
1824
users: {
1925
id: string | null;
2026
display_name: string | null;
@@ -36,7 +42,9 @@ interface PlanDetailsClientProps {
3642
export default function PlanDetailsClient({
3743
plan,
3844
platformUser,
45+
isCreator,
3946
users,
47+
polls,
4048
timeblocks,
4149
}: PlanDetailsClientProps) {
4250
const { resolvedTheme } = useTheme();
@@ -139,17 +147,24 @@ export default function PlanDetailsClient({
139147
platformUser={platformUser}
140148
handlePNG={downloadAsPNG}
141149
/>
150+
142151
<div id="plan-ref" className="flex w-full flex-col items-center">
143152
<p className="my-4 flex max-w-xl items-center gap-2 text-center text-2xl leading-tight! font-semibold md:mb-4 lg:text-3xl">
144153
{plan.name} <EditPlanDialog plan={plan} />
145154
</p>
146-
<div className="mt-8 grid w-full items-center justify-between gap-4 md:grid-cols-2">
155+
<div className="mt-8 grid w-full items-center justify-between gap-4 md:grid-cols-2 lg:grid-cols-3">
147156
<PlanLogin
148157
plan={plan}
149158
timeblocks={[]}
150159
platformUser={platformUser}
151160
/>
152161
<AllAvailabilities plan={plan} timeblocks={timeblocks} />
162+
<PlanDetailsPolls
163+
plan={plan}
164+
polls={polls}
165+
isCreator={isCreator}
166+
platformUser={platformUser}
167+
/>
153168
</div>
154169
</div>
155170
</div>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client';
2+
3+
import { useTimeBlocking } from '@/app/[locale]/(marketing)/meet-together/plans/[planId]/time-blocking-provider';
4+
import type {
5+
GetPollsForPlanResult,
6+
MeetTogetherPlan,
7+
} from '@tuturuuu/types/primitives/MeetTogetherPlan';
8+
import type { User } from '@tuturuuu/types/primitives/User';
9+
import MultipleChoiceVote from '@tuturuuu/ui/legacy/tumeet/multiple-choice-vote';
10+
11+
interface PlanDetailsPollsProps {
12+
plan: MeetTogetherPlan;
13+
isCreator: boolean;
14+
platformUser: User | null;
15+
polls: GetPollsForPlanResult | null;
16+
}
17+
18+
export default function PlanDetailsPolls({
19+
plan,
20+
isCreator,
21+
platformUser,
22+
polls,
23+
}: PlanDetailsPollsProps) {
24+
const { user: guestUser } = useTimeBlocking();
25+
26+
const user = guestUser ?? platformUser;
27+
const currentUserId = user?.id ?? null;
28+
const userType =
29+
user?.is_guest === true
30+
? 'GUEST'
31+
: platformUser?.id
32+
? 'PLATFORM'
33+
: 'DISPLAY';
34+
35+
// You might want to only show the "where" poll or map over all polls
36+
const wherePoll = polls?.polls?.[0]; // Assuming the first poll is the "where to meet" poll
37+
38+
const onVote = async (pollId: string, optionIds: string[]) => {
39+
await fetch(`/api/meet-together/plans/${plan.id}/poll/vote`, {
40+
method: 'POST',
41+
body: JSON.stringify({
42+
pollId,
43+
optionIds,
44+
userType, // 'PLATFORM' or 'GUEST'
45+
guestId: userType === 'GUEST' ? user?.id : undefined,
46+
}),
47+
headers: { 'Content-Type': 'application/json' },
48+
});
49+
};
50+
51+
const onAddOption = async (pollId: string, value: string) => {
52+
const res = await fetch(`/api/meet-together/plans/${plan.id}/poll/option`, {
53+
method: 'POST',
54+
body: JSON.stringify({
55+
pollId,
56+
value,
57+
userType, // 'PLATFORM' or 'GUEST'
58+
guestId: userType === 'GUEST' ? user?.id : undefined,
59+
}),
60+
headers: { 'Content-Type': 'application/json' },
61+
});
62+
63+
console.log('Option added:', res);
64+
};
65+
66+
if (!plan.where_to_meet && !isCreator) {
67+
return null; // Don't render anything if "where to meet" is not enabled and user is not creator
68+
}
69+
70+
return (
71+
<div className="sticky top-16 z-10 self-start rounded-lg border px-2 py-4 md:px-4">
72+
{plan.where_to_meet && wherePoll ? (
73+
<MultipleChoiceVote
74+
pollName={wherePoll.name}
75+
pollId={wherePoll.id}
76+
options={wherePoll.options}
77+
currentUserId={currentUserId}
78+
isDisplayMode={userType === 'DISPLAY'}
79+
onAddOption={onAddOption}
80+
onVote={onVote}
81+
/>
82+
) : (
83+
isCreator && (
84+
<div className="flex flex-col gap-4">
85+
<p className="text-sm text-gray-500">
86+
You can enable "Where to meet" voting for your plan.
87+
</p>
88+
<button
89+
className="rounded bg-dynamic-blue px-4 py-2 font-medium text-white shadow transition hover:bg-dynamic-blue/80"
90+
onClick={async () => {
91+
// call your backend here to enable where_to_meet
92+
// await onUpdateWhereToMeet(true);
93+
}}
94+
>
95+
Enable "Where to meet" voting
96+
</button>
97+
</div>
98+
)
99+
)}
100+
</div>
101+
);
102+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// /api/meet-together/poll/option.ts
2+
import {
3+
createAdminClient,
4+
createClient,
5+
} from '@tuturuuu/supabase/next/server';
6+
import { NextResponse } from 'next/server';
7+
8+
export async function POST(req: Request) {
9+
const { pollId, value, userType, guestId } = await req.json();
10+
const sbAdmin = await createAdminClient();
11+
const supabase = await createClient();
12+
13+
let userId: string | null = null;
14+
if (userType === 'PLATFORM') {
15+
const {
16+
data: { user },
17+
} = await supabase.auth.getUser();
18+
userId = user?.id ?? null;
19+
if (!userId)
20+
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
21+
}
22+
23+
// Insert new poll option (open to both guests and users)
24+
const { data: option, error } = await sbAdmin
25+
.from('poll_options')
26+
.insert({
27+
poll_id: pollId,
28+
value,
29+
})
30+
.select('id')
31+
.single();
32+
33+
if (error) {
34+
return NextResponse.json(
35+
{ message: 'Failed to add option', error },
36+
{ status: 500 }
37+
);
38+
}
39+
40+
// Optionally: Auto-vote for the new option
41+
if (userType === 'PLATFORM' && userId) {
42+
await sbAdmin.from('poll_user_votes').insert({
43+
user_id: userId,
44+
option_id: option.id,
45+
});
46+
} else if (userType === 'GUEST' && guestId) {
47+
await sbAdmin.from('poll_guest_votes').insert({
48+
guest_id: guestId,
49+
option_id: option.id,
50+
});
51+
}
52+
53+
return NextResponse.json({
54+
message: 'Option added and voted',
55+
optionId: option.id,
56+
});
57+
}

0 commit comments

Comments
 (0)