Skip to content

Commit

Permalink
Merge pull request #85 from simonyiszk/feature/limit-questions-per-pr…
Browse files Browse the repository at this point in the history
…esentation

Limit questions per presentation
  • Loading branch information
Tschonti authored Mar 18, 2024
2 parents 9435d49 + 0a48220 commit 5c85cd2
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ JWT_SCOPE="<required scope for the service account>"
GROUP_KEY="<email address of the google group>"
BACKEND_URL="https://..."
NEXT_PUBLIC_PLAUSIBLE_URL="<plausible url>"
NEXT_PUBLIC_RECAPTCHA_SITE_KEY="<>"
RECAPTCHA_SECRET="<>"
4 changes: 3 additions & 1 deletion src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ export async function sendQuestion({
question,
slug,
recaptchaToken,
userId,
}: {
question: string;
slug: string;
recaptchaToken: string;
userId: string;
}) {
const isRecaptchaValid = await validateRecaptcha(recaptchaToken);
if (!isRecaptchaValid) {
Expand All @@ -54,7 +56,7 @@ export async function sendQuestion({
}
const res = await fetch(`https://konf-qna.kir-dev.hu/api/presentation/${slug}/question`, {
method: 'POST',
body: JSON.stringify({ content: question, userId: 'zokni' }),
body: JSON.stringify({ content: question, userId }),
});
if (res.status === 200) {
return 201;
Expand Down
20 changes: 4 additions & 16 deletions src/app/questions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { RoomQuestion } from '@/components/tiles/question-tile';
import { QuestionPageBody } from '@/components/questions/question-page-body';
import { getDelayData } from '@/models/get-delay-data';
import { getPresentationData } from '@/models/get-presentation-data';

export default async function questionsPage() {
const presentations = await getPresentationData();
const delay = await getDelayData();
return (
<div className='flex flex-col px-4 sm:px-6 xl:px-0 max-w-6xl w-full overflow-hidden'>
<h1 className='mb-16 mt-8'>Kérdezz az elődóktól!</h1>

<div className='grid grid-cols-1 sm:grid-cols-2 gap-6'>
<div className='order-1'>
<h2 className='text-4xl text-center'>IB028</h2>
</div>
<div className='order-3 sm:order-2'>
<h2 className='text-4xl text-center'>IB025</h2>
</div>
<div className='order-2 sm:order-3'>
<RoomQuestion presentations={presentations ?? []} room='IB028' />
</div>
<div className='order-4'>
<RoomQuestion presentations={presentations ?? []} room='IB025' />
</div>
</div>
<QuestionPageBody presentations={presentations} delay={delay ?? 0} />
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/navbar/desktop-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function DesktopNavbar() {
}, []);

return (
<nav className='w-full mx-auto hidden md:flex justify-end items-center flex-wrap gap-10 flex-col md:flex-row fixed p-5 top-0 z-20 overflow-hidden'>
<nav className='w-full mx-auto hidden lg:flex justify-end items-center flex-wrap gap-10 flex-col lg:flex-row fixed p-5 top-0 z-20 overflow-hidden'>
<div
id='desktop-nav-container'
className={clsx(
Expand Down
2 changes: 1 addition & 1 deletion src/components/navbar/navbar-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const links = [

export function NavbarItems() {
return (
<div className='flex flex-col md:flex-row gap-3 mt-5 md:mt-0 md:gap-10'>
<div className='flex flex-col lg:flex-row gap-3 mt-5 lg:mt-0 lg:gap-10'>
{links.map(({ href, label, external }) => (
<Link
href={href}
Expand Down
2 changes: 1 addition & 1 deletion src/components/navbar/navbar-mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function NavbarMobile() {
}, []);

return (
<nav className='md:hidden overflow-hidden'>
<nav className='lg:hidden overflow-hidden'>
<div className='w-full px-5 py-3 fixed top-0 z-20' onClick={onLinkClick}>
<div
id='mobile-nav-container'
Expand Down
9 changes: 2 additions & 7 deletions src/components/presentation/PresentationGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import clsx from 'clsx';
import Link from 'next/link';
import React, { CSSProperties, useRef } from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';

import { PresentationQuestionForm } from '@/components/presentation/PresentationQuestion';
import { Tile } from '@/components/tiles/tile';
Expand Down Expand Up @@ -102,7 +101,7 @@ export function PresentationTile({
<Tile clickable={!presentation.placeholder && !preview} className='w-full h-full' disableMinHeight={true}>
<Tile.Body lessPadding='5' className='flex flex-col'>
<span className='pb-2 text-xs'>
{presentation.room !== 'BOTH' && `${presentation.room} | `}
{presentation.room !== 'BOTH' && !preview && `${presentation.room} | `}
{dateToHourAndMinuteString(presentation.startDate)} - {dateToHourAndMinuteString(presentation.endDate)}
</span>
<div className='flex flex-col justify-center flex-1'>
Expand Down Expand Up @@ -144,11 +143,7 @@ export function PresentationTile({
{presentation.presenter?.company?.category === SponsorCategory.MAIN_SPONSOR && !preview && (
<p className='mt-2 text-base whitespace-pre-line'>{presentation.description.split('\n')[0]}</p>
)}
{preview && (
<GoogleReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}>
<PresentationQuestionForm slug={presentation.slug} />
</GoogleReCaptchaProvider>
)}
{preview && <PresentationQuestionForm slug={presentation.slug} />}
</div>
</Tile.Body>
</Tile>
Expand Down
58 changes: 42 additions & 16 deletions src/components/presentation/PresentationQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Dialog } from '@headlessui/react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import { FaCheckCircle } from 'react-icons/fa';

import { sendQuestion } from '@/app/actions';
import { WhiteButton } from '@/components/white-button';
import { AllowedQuestionCount, getQuestionCount, getUserId, incrementQuestionCount } from '@/utils/questionHelpers';

interface PresentationQuestionFormProps {
slug: string;
Expand All @@ -17,18 +18,28 @@ export function PresentationQuestionForm({ slug }: PresentationQuestionFormProps
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [question, setQuestion] = useState('');
const [questionCount, setQuestionCount] = useState(0);

const canAskQuestions = questionCount < AllowedQuestionCount;

useEffect(() => {
setQuestionCount(getQuestionCount(slug));
}, []);

const onSend = async () => {
if (!executeRecaptcha) return;
const recaptchaToken = await executeRecaptcha('presentation_question');
if (question.trim()) {
if (question.trim() && canAskQuestions) {
setIsLoading(true);
const status = await sendQuestion({ question, slug, recaptchaToken });
const status = await sendQuestion({ question, slug, recaptchaToken, userId: getUserId() });
setIsLoading(false);
switch (status) {
case 201:
incrementQuestionCount(slug);
setQuestionCount(questionCount + 1);
setIsSuccessOpen(true);
setQuestion('');
setError('');
break;
case 400:
setError('Hibás formátum!');
Expand All @@ -41,19 +52,34 @@ export function PresentationQuestionForm({ slug }: PresentationQuestionFormProps

return (
<div className='mt-10 w-full'>
<textarea
className='w-full rounded-md p-2 bg-transparent border-white border-[0.5px]'
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={4}
placeholder='Ide írd a kérdésed!'
/>
{error && <p className='text-red-500 my-2'>{error}</p>}
<div className='w-full my-4 flex justify-center'>
<WhiteButton onClick={onSend} disabled={!question.trim() || isLoading || !executeRecaptcha}>
Kérdés küldése
</WhiteButton>
</div>
{canAskQuestions ? (
<>
<div className='relative'>
<textarea
className='w-full rounded-md p-2 bg-transparent border-white border-[0.5px]'
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={4}
placeholder='Ide írd a kérdésed!'
/>
<p className='absolute right-0 bottom-0 p-4'>
{questionCount}/{AllowedQuestionCount} Kérdés feltéve
</p>
</div>
{error && <p className='text-red-500 my-2'>{error}</p>}
<div className='w-full my-4 flex justify-center'>
<WhiteButton onClick={onSend} disabled={!question.trim() || isLoading || !executeRecaptcha}>
Kérdés küldése
</WhiteButton>
</div>
</>
) : (
<div className='w-full my-4 flex justify-center'>
<WhiteButton disabled={true} onClick={() => {}}>
Elfogytak a feltehető kérdések!
</WhiteButton>
</div>
)}
<Dialog open={isSuccessOpen} onClose={() => setIsSuccessOpen(false)} className='relative z-50'>
<div className='fixed inset-0 bg-black/80' aria-hidden='true' />

Expand Down
33 changes: 33 additions & 0 deletions src/components/questions/question-page-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';

import { RoomQuestion } from '@/components/tiles/question-tile';
import { PresentationWithDates } from '@/models/models';

export function QuestionPageBody({
presentations,
delay,
}: {
presentations: PresentationWithDates[] | undefined;
delay: number;
}) {
return (
<GoogleReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-6'>
<div className='order-1'>
<h2 className='text-4xl text-center'>IB028</h2>
</div>
<div className='order-3 sm:order-2 mt-16 sm:mt-0'>
<h2 className='text-4xl text-center'>IB025</h2>
</div>
<div className='order-2 sm:order-3'>
<RoomQuestion presentations={presentations ?? []} room='IB028' delay={delay} />
</div>
<div className='order-4'>
<RoomQuestion presentations={presentations ?? []} room='IB025' delay={delay} />
</div>
</div>
</GoogleReCaptchaProvider>
);
}
36 changes: 30 additions & 6 deletions src/components/tiles/question-tile.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
'use client';
import { useEffect, useState } from 'react';

import { PresentationWithDates } from '@/models/models';
import { getCurrentDate } from '@/utils/dateHelper';

import { PresentationTile } from '../presentation/PresentationGrid';

type Props = {
presentations: PresentationWithDates[];
room: 'IB028' | 'IB025';
delay: number;
};
const MARGIN_MINUTES = 10;

export function RoomQuestion({ presentations, room, delay }: Props) {
const [now, setNow] = useState(new Date(0));

useEffect(() => {
setNow(getNow(delay));
const id = setInterval(() => setNow(getNow(delay)), 30000);
return () => clearInterval(id);
}, []);

export function RoomQuestion({ presentations, room }: Props) {
const now = getCurrentDate();
const presentation = presentations.find((p) => p.room === room && p.startDate < now && p.endDate > now);
return presentation ? (
<PresentationTile presentation={presentation} preview />
const presentationInRoom = presentations.filter((p) => {
const endDateWithMargin = new Date(p.endDate);
endDateWithMargin.setMinutes(endDateWithMargin.getMinutes() + MARGIN_MINUTES);
return p.room === room && !p.placeholder && p.startDate < now && endDateWithMargin > now;
});
return presentationInRoom.length > 0 ? (
<div className='flex flex-col gap-4'>
{presentationInRoom.reverse().map((p) => (
<PresentationTile presentation={p} key={p.slug} preview />
))}
</div>
) : (
<p className='text-stone-200 text-base sm:text-[20px] text-center'>
Jelenleg nincs előadás ebben a teremben, nézz vissza később!
</p>
);
}

function getNow(delay: number): Date {
const now = new Date();
now.setMinutes(now.getMinutes() - delay);
return now;
}
9 changes: 9 additions & 0 deletions src/models/get-delay-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export async function getDelayData() {
const response = await fetch(`${process.env.BACKEND_URL}/proto/delay`, { cache: 'no-store' });
if (!response.ok) {
console.error(response);
return;
}
const data: { delay: number } = await response.json();
return data.delay;
}
5 changes: 0 additions & 5 deletions src/utils/dateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,3 @@ export function dateToHourAndMinuteString(date: Date): string {
timeZone: 'Europe/Budapest',
});
}

export function getCurrentDate() {
return new Date(2024, 2, 19, 14, 12);
// return new Date();
}
31 changes: 31 additions & 0 deletions src/utils/questionHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';

export const AllowedQuestionCount = 3;

export function getUserId(): string {
let key = localStorage.getItem('user-id');
if (!key) {
key = generateRandomString(32);
localStorage.setItem('user-id', key);
}
return key;
}

export function getQuestionCount(slug: string) {
let amount = Number(localStorage.getItem(`question-${slug}`));
if (isNaN(amount)) {
amount = 0;
localStorage.setItem(`question-${slug}`, String(amount));
}
return amount;
}

export function incrementQuestionCount(slug: string) {
localStorage.setItem(`question-${slug}`, String(getQuestionCount(slug) + 1));
}

function generateRandomString(length: number): string {
return Array.from({ length })
.map(() => chars[Math.floor(Math.random() * chars.length)])
.join('');
}

0 comments on commit 5c85cd2

Please sign in to comment.