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

questions from the web #84

Merged
merged 6 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
41 changes: 41 additions & 0 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,44 @@ export async function addToGroup({ email }: { email: string }) {
return 500;
}
}

export async function sendQuestion({
question,
slug,
recaptchaToken,
}: {
question: string;
slug: string;
recaptchaToken: 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: 'zokni' }),
});
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;
}
26 changes: 26 additions & 0 deletions src/app/questions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RoomQuestion } from '@/components/tiles/question-tile';
import { getPresentationData } from '@/models/get-presentation-data';

export default async function questionsPage() {
const presentations = await getPresentationData();
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>
</div>
);
}
4 changes: 4 additions & 0 deletions 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 Down
99 changes: 57 additions & 42 deletions src/components/presentation/PresentationGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import clsx from 'clsx';
import Link from 'next/link';
import { CSSProperties, useRef } from 'react';
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';
import { PresentationWithDates, SponsorCategory } from '@/models/models';
import { dateToHourAndMinuteString } from '@/utils/dateHelper';
Expand Down Expand Up @@ -88,56 +90,69 @@ 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' && `${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 && (
<GoogleReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}>
<PresentationQuestionForm slug={presentation.slug} />
</GoogleReCaptchaProvider>
)}
</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
75 changes: 75 additions & 0 deletions src/components/presentation/PresentationQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Dialog } from '@headlessui/react';
import { 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';

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 onSend = async () => {
if (!executeRecaptcha) return;
const recaptchaToken = await executeRecaptcha('presentation_question');
if (question.trim()) {
setIsLoading(true);
const status = await sendQuestion({ question, slug, recaptchaToken });
setIsLoading(false);
switch (status) {
case 201:
setIsSuccessOpen(true);
setQuestion('');
break;
case 400:
setError('Hibás formátum!');
break;
default:
setError('Ismeretlen hiba!');
}
}
};

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>
<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>
);
}
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
20 changes: 10 additions & 10 deletions src/components/tiles/mobile/android-tile.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import Image from 'next/image';
import Link from 'next/link';

import { Tile } from '@/components/tiles/tile';

interface AndroidTileProps {
androidUrl: string;
}

export function AndroidTile({ androidUrl }: AndroidTileProps) {
export function AndroidTile({}: AndroidTileProps) {
return (
<Tile>
<Tile.Body className='space-y-8 text-center'>
<h2>Android</h2>
<Link className='mx-auto w-fit block' href={androidUrl}>
<Image src='/img/androidapk.svg' alt='Letölthető Android APK' width={302} height={80} />
</Link>
<p>
Az APK fájlt letöltve és megnyitva telepítheted az alkalmazást az Androidos eszközödre. A telepítés
előtt/közben az "Ismeretlen forrásból származó alkalmazások engedélyezése" opciót be kell kapcsolni a
beállításokban.
</p>
<Image
className='opacity-50 mx-auto'
src='/img/google-play.png'
alt='Letölthető Android APK'
width={250}
height={40}
/>
<p>Sajnáljuk, de az Android alkalmazás még nem érhető el a Google Play Áruházban.</p>
<p>További információkért látogass el a Kir-Dev standjához!</p>
</Tile.Body>
</Tile>
);
Expand Down
22 changes: 22 additions & 0 deletions src/components/tiles/question-tile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';
import { PresentationWithDates } from '@/models/models';
import { getCurrentDate } from '@/utils/dateHelper';

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

type Props = {
presentations: PresentationWithDates[];
room: 'IB028' | 'IB025';
};

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 />
) : (
<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>
);
}
5 changes: 5 additions & 0 deletions src/utils/dateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export function dateToHourAndMinuteString(date: Date): string {
timeZone: 'Europe/Budapest',
});
}

export function getCurrentDate() {
return new Date(2024, 2, 19, 14, 12);
// return new Date();
}
Loading
Loading