Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4.0 #86

Merged
merged 13 commits into from
Mar 18, 2024
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>
);
}
3 changes: 0 additions & 3 deletions src/components/tiles/mobil-app-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ export function MobilAppTile({ data }: Props) {
<a href={data.iosUrl} target='_blank'>
<img className='h-[60px] min-w-[180]' src='/img/appstore.svg' alt='App Store' />
</a>
<a href={data.androidUrl} className='h-fit' target='_blank'>
<img className='h-[57px] min-w-[190]' src='/img/androidapk.svg' alt='Play Store' />
</a>
</div>
<Link
href='/mobile'
Expand Down
Loading
Loading