Skip to content

Commit

Permalink
Merge pull request #86 from simonyiszk/dev
Browse files Browse the repository at this point in the history
v4.0
  • Loading branch information
Tschonti authored Mar 18, 2024
2 parents 3df1f3a + 5c85cd2 commit 8afdd4d
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 47 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="<>"
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"react-markdown": "^9.0.1",
"react-snap-carousel": "^0.4.0",
"remark-gfm": "^4.0.0",
"yet-another-react-lightbox": "^3.16.0"
"yet-another-react-lightbox": "^3.16.0",
"react-google-recaptcha-v3": "^1.10.1"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
43 changes: 43 additions & 0 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,46 @@ export async function addToGroup({ email }: { email: string }) {
return 500;
}
}

export async function sendQuestion({
question,
slug,
recaptchaToken,
userId,
}: {
question: string;
slug: string;
recaptchaToken: string;
userId: string;
}) {
const isRecaptchaValid = await validateRecaptcha(recaptchaToken);
if (!isRecaptchaValid) {
console.error('Recaptcha validation failed');
return 400;
}
if (!question || !slug) {
return 400;
}
const res = await fetch(`https://konf-qna.kir-dev.hu/api/presentation/${slug}/question`, {
method: 'POST',
body: JSON.stringify({ content: question, userId }),
});
if (res.status === 200) {
return 201;
} else if (res.status === 400) {
return 400;
}
return 500;
}

async function validateRecaptcha(token: string) {
const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `secret=${process.env.RECAPTCHA_SECRET}&response=${token}`,
});
const data = await res.json();
return data.success;
}
14 changes: 14 additions & 0 deletions src/app/questions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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>
<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
6 changes: 5 additions & 1 deletion src/components/navbar/navbar-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const links = [
href: '/contact',
label: 'kapcsolat',
},
{
href: '/questions',
label: 'kérdések',
},
{
href: '/golya',
label: 'gólyáknak',
Expand All @@ -27,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
94 changes: 52 additions & 42 deletions src/components/presentation/PresentationGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import clsx from 'clsx';
import Link from 'next/link';
import { CSSProperties, useRef } from 'react';
import React, { CSSProperties, useRef } from 'react';

import { PresentationQuestionForm } from '@/components/presentation/PresentationQuestion';
import { Tile } from '@/components/tiles/tile';
import { PresentationWithDates, SponsorCategory } from '@/models/models';
import { dateToHourAndMinuteString } from '@/utils/dateHelper';
Expand Down Expand Up @@ -88,56 +89,65 @@ export function PresentationGrid({
);
}

function PresentationTile({ presentation }: { presentation: PresentationWithDates }) {
export function PresentationTile({
presentation,
preview = false,
}: {
presentation: PresentationWithDates;
preview?: boolean;
}) {
return (
<Tile clickable={!presentation.placeholder} 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} | `}
{dateToHourAndMinuteString(presentation.startDate)} - {dateToHourAndMinuteString(presentation.endDate)}
</span>
<div className='flex flex-col justify-center flex-1'>
<div className={clsx('flex', presentation.placeholder && 'justify-around')}>
<h2
className={clsx(
'text-lg lg:text-xl font-medium',
!presentation.presenter ? 'text-center pb-4' : 'pb-4 lg:pb-6'
)}
>
{presentation.title}
</h2>
{presentation.room === 'BOTH' && presentation.placeholder && (
<>
<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' && !preview && `${presentation.room} | `}
{dateToHourAndMinuteString(presentation.startDate)} - {dateToHourAndMinuteString(presentation.endDate)}
</span>
<div className='flex flex-col justify-center flex-1'>
<div className={clsx('flex', presentation.placeholder && 'justify-around')}>
<h2
aria-hidden={true}
className={clsx(
'text-lg lg:text-xl pb-4 lg:pb-6 font-medium',
!presentation.presenter && 'text-center'
'text-lg lg:text-xl font-medium',
!presentation.presenter ? 'text-center pb-4' : 'pb-4 lg:pb-6'
)}
>
{presentation.title}
</h2>
{presentation.room === 'BOTH' && presentation.placeholder && (
<h2
aria-hidden={true}
className={clsx(
'text-lg lg:text-xl pb-4 lg:pb-6 font-medium',
!presentation.presenter && 'text-center'
)}
>
{presentation.title}
</h2>
)}
</div>
{!!presentation.presenter && (
<div className='flex gap-4'>
<img
src={presentation.presenter.pictureUrl}
className='object-cover rounded-3xl w-16 h-16'
alt='Presentation Image'
/>
<div>
<h3 className='text-lg lg:text-2xl font-bold text-[#FFE500]'>{presentation.presenter.name}</h3>
<div className='text-xs lg:text-sm'>{presentation.presenter.rank}</div>
<div className='hidden lg:block text-xs pt-0.5'>{presentation.presenter.company?.name}</div>
</div>
</div>
)}
{presentation.presenter?.company?.category === SponsorCategory.MAIN_SPONSOR && !preview && (
<p className='mt-2 text-base whitespace-pre-line'>{presentation.description.split('\n')[0]}</p>
)}
{preview && <PresentationQuestionForm slug={presentation.slug} />}
</div>
{!!presentation.presenter && (
<div className='flex gap-4'>
<img
src={presentation.presenter.pictureUrl}
className='object-cover rounded-3xl w-16 h-16'
alt='Presentation Image'
/>
<div>
<h3 className='text-lg lg:text-2xl font-bold text-[#FFE500]'>{presentation.presenter.name}</h3>
<div className='text-xs lg:text-sm'>{presentation.presenter.rank}</div>
<div className='hidden lg:block text-xs pt-0.5'>{presentation.presenter.company?.name}</div>
</div>
</div>
)}
{presentation.presenter?.company?.category === SponsorCategory.MAIN_SPONSOR && (
<p className='mt-2 text-base whitespace-pre-line'>{presentation.description.split('\n')[0]}</p>
)}
</div>
</Tile.Body>
</Tile>
</Tile.Body>
</Tile>
</>
);
}

Expand Down
101 changes: 101 additions & 0 deletions src/components/presentation/PresentationQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Dialog } from '@headlessui/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;
}

export function PresentationQuestionForm({ slug }: PresentationQuestionFormProps) {
const { executeRecaptcha } = useGoogleReCaptcha();
const [isSuccessOpen, setIsSuccessOpen] = useState(false);

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() && canAskQuestions) {
setIsLoading(true);
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!');
break;
default:
setError('Ismeretlen hiba!');
}
}
};

return (
<div className='mt-10 w-full'>
{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' />

<div className='fixed inset-0 flex w-screen items-center justify-center p-4'>
<Dialog.Panel className='mx-auto max-w-lg rounded bg-[#0f181c] p-8 flex flex-col items-center gap-5'>
<div className='text-8xl text-white'>
<FaCheckCircle />
</div>
<Dialog.Title className='font-bold text-2xl mb-5 text-center'>
A kérdésed megkaptuk és moderálás után a felolvasandó kérdések közé kerül. Köszönjük!
</Dialog.Title>

<WhiteButton onClick={() => setIsSuccessOpen(false)}>Rendben</WhiteButton>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
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>
);
}
Loading

0 comments on commit 8afdd4d

Please sign in to comment.