Skip to content

Commit

Permalink
Feat/kiosk (#314)
Browse files Browse the repository at this point in the history
* WIP

* Pexeso WIP

* Inactivity modal for game page

* KidsDictionaryGame - add support for kiosk render

* Kiosk custom scrollbar

* Disable zoom

* Viewport

* Animate card

* Package update + card bg on active

* Fix support for UA language

* Fix SK and PL builds

* Refactor

* Stories for kiosk

* Update tailwind to look in utils

* Code review updates

* Move props to components

* Quiz game added

* Add confetti animation on correct

* Do not show confetti on web

* Remove enum, support all langs

* Hide scrollbar if not needed

* Remove currenPlatform atom

---------

Co-authored-by: Martin Starosta <[email protected]>
  • Loading branch information
martin-starosta and Martin Starosta committed Jun 25, 2023
1 parent e01966c commit bb944df
Show file tree
Hide file tree
Showing 49 changed files with 12,459 additions and 664 deletions.
13 changes: 13 additions & 0 deletions @types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** Stories */
export interface Story {
title: Record<string, string>;
slug: string;
duration: string;
country: string;
}

/* ENUMS */
export enum Platform {
KIOSK = 'kiosk',
WEB = 'web',
}
32 changes: 32 additions & 0 deletions components/basecomponents/KidsDictionaryList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Components */
import KidsTranslationContainer from './KidsTranslationContainer';

/* Hooks, Types, Utils */
import { normalizeForId } from 'utils/textNormalizationUtils';
import { getCountryVariant } from 'utils/locales';
import { Category } from 'utils/getDataUtils';

export type KidsCategoryListProps = {
kidsCategory: Category | undefined;
};

const KidsDictionaryList: React.FC<KidsCategoryListProps> = ({ kidsCategory }) => {
if (!kidsCategory?.translations) {
return null;
}

return (
<>
{kidsCategory.translations.map((phrase, index) => (
<KidsTranslationContainer
key={`${phrase.getTranslation('uk')}-${index}`}
id={normalizeForId(phrase.getTranslation(getCountryVariant()))}
imageUrl={phrase.getImageUrl()}
phrase={phrase}
/>
))}
</>
);
};

export default KidsDictionaryList;
139 changes: 109 additions & 30 deletions components/basecomponents/KidsTranslation.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,124 @@
import { useCallback } from 'react';
import { useTranslation, Trans } from 'next-i18next';
import React, { useCallback, useState } from 'react';

/** Components */
import PlayKidsIcon from '../../public/icons/play.svg';
import { Language } from '../../utils/locales';
import { AudioPlayer } from 'utils/AudioPlayer';
import { Flag } from './Flag';
import { AudioPlayer } from 'utils/AudioPlayer';

/** Hooks, Types, Utils */
import styles from './KidsTranslation.module.css';
interface KidsTranslationProps {
import { Platform } from '@types';
import { Language } from 'utils/locales';
import { useAtom } from 'jotai';
import { dictionaryAudioPlayAtom, dictionaryActivePhraseAtom } from 'components/basecomponents/Kiosk/atoms';
import { usePlatform } from 'utils/usePlatform';
const PLAY_ACTION_TRANSLATION_ID = 'utils.play';
const KIOSK_BG_COLOR_UK = '#FFF7D5';
const KIOSK_BG_COLOR_OTHER = '#FFE1DE';

type KidsTranslationProps = {
translation: string;
transcription: string;
soundUrl: string;
language: Language;
}
isActive?: boolean;
isCard?: boolean;
};

export const KidsTranslation = ({
transcription,
translation,
language,
soundUrl,
isActive = false,
isCard = false,
}: KidsTranslationProps): JSX.Element => {
const [audioPlaying, setIsPlaying] = useAtom(dictionaryAudioPlayAtom);
const [activePhrase, setActivePhrase] = useAtom(dictionaryActivePhraseAtom);
const renderFor = usePlatform();

export const KidsTranslation = ({ transcription, translation, language, soundUrl }: KidsTranslationProps): JSX.Element => {
const { t } = useTranslation();
const [isPlaying, setIsPlaying] = useState(false);
const handleClick = useCallback(async () => {
if (!isPlaying) {
setIsPlaying(true);
await AudioPlayer.getInstance().playSrc(soundUrl);
setIsPlaying(false);
if (audioPlaying) {
return;
}
}, [isPlaying, soundUrl]);

return (
<div className="flex justify-between items-center py-2 ">
<div className="w-full">
<div className="flex items-center mb-2">
<Flag language={language} width={30} height={30} className={'mr-3'} />
<p>
<Trans className="block my-2">{t(`dictionary_page.${language}`)}</Trans>
</p>
setActivePhrase(translation);
setIsPlaying(true);
await AudioPlayer.getInstance().playSrc(soundUrl);
setIsPlaying(false);
}, [audioPlaying, soundUrl, setIsPlaying, setActivePhrase, translation]);

/* Kids Translation as default (web site) */
const renderDefault = () => {
return (
<div className="flex justify-between items-center py-2 ">
<div className="w-full">
<div className="flex items-center mb-2">
<Flag language={language} width={30} height={30} className={'mr-3'} />
<p>
<Trans className="block my-2">{t(`dictionary_page.${language}`)}</Trans>
</p>
</div>
<p className="self-start w-full font-semibold">{translation}</p>
<p className="text-gray-500">{`[\u00A0${transcription}\u00A0]`}</p>
</div>
<p className="self-start w-full font-semibold">{translation}</p>
<p className="text-gray-500">{`[\u00A0${transcription}\u00A0]`}</p>
<button onClick={handleClick} aria-label={`${t(PLAY_ACTION_TRANSLATION_ID)} ${translation}`}>
<PlayKidsIcon
className={`cursor-pointer active:scale-75 transition-all duration-300 w-14 ${styles.playIcon} ${
activePhrase === translation && audioPlaying ? styles.pulse : ''
}`}
/>
</button>
</div>
<button onClick={handleClick} aria-label={t('utils.play') + ' ' + translation}>
<PlayKidsIcon
className={`cursor-pointer active:scale-75 transition-all duration-300 w-14 ${styles.playIcon} ${isPlaying ? styles.pulse : ''}`}
/>
</button>
</div>
);
);
};

/** Kids Translation as card */
const renderAsCard = () => {
return (
<div className="flex justify-between items-center py-3 px-5 bg-white rounded-xl shadow-xl mb-20">
<div className="w-full">
<div className="flex items-center mb-2 w-56">
<Flag language={language} width={30} height={30} className={'mr-3'} />
<p>
<Trans className="block my-2">{t(`dictionary_page.${language}`)}</Trans>
</p>
</div>
<p className="self-start w-full">{translation}</p>
<p className="text-gray-500">{`[\u00A0${transcription}\u00A0]`}</p>
</div>
<button onClick={handleClick} aria-label={`${t(PLAY_ACTION_TRANSLATION_ID)} ${translation}`}>
<PlayKidsIcon
className={`cursor-pointer active:scale-75 transition-all duration-300 w-24 ${styles.playIcon} ${
activePhrase === translation && audioPlaying ? styles.pulse : ''
}`}
/>
</button>
</div>
);
};

/** Kids Translation for kids kiosk */
const renderForKiosk = () => {
const playButtonClasses = `flex grow ${language === 'uk' ? `bg-[${KIOSK_BG_COLOR_UK}]` : `bg-[${KIOSK_BG_COLOR_OTHER}]`} ${
isActive ? 'flex-row w-full justify-center items-center h-[122px]' : 'flex-col justify-between items-center py-5 w-1/2'
}`;
return (
<div className={playButtonClasses} onClick={handleClick} aria-label={`${t(PLAY_ACTION_TRANSLATION_ID)} ${translation}`}>
<div className="flex items-center mb-2 z-50">
<Flag language={language} width={50} height={50} className={'mr-3'} />
</div>
<p className={`text-center font-semibold ${isActive ? 'text-2xl' : ''}`}>{translation}</p>
</div>
);
};

switch (renderFor) {
case Platform.KIOSK:
return isCard ? renderAsCard() : renderForKiosk();
case Platform.WEB:
default:
return renderDefault();
}
};
115 changes: 79 additions & 36 deletions components/basecomponents/KidsTranslationContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,96 @@
import React from 'react';
import Image from 'next/legacy/image';
import { useAtomValue } from 'jotai';

/** Components */
import { KidsTranslation } from './KidsTranslation';
import KioskDictionaryCardImage from './KioskDictionaryGame/KioskDictionaryCardImage';

/** Hooks, Types, Utils, etc. */
import { useLanguage } from 'utils/useLanguageHook';
import { AudioPlayer } from 'utils/AudioPlayer';
import { useTranslation } from 'next-i18next';
import { Phrase } from '../../utils/getDataUtils';
import { Phrase } from 'utils/getDataUtils';
import { Platform } from '@types';
import { dictionaryAudioPlayAtom, dictionaryActivePhraseAtom } from './Kiosk/atoms';
import { Language } from 'utils/locales';
import { usePlatform } from 'utils/usePlatform';

interface KidsTranslationContainerProps {
export type KidsTranslationContainerProps = {
phrase: Phrase;
imageUrl: string | null;
id?: string;
searchText?: string;
}

/**
* Displays list of translations in opened collapse component
*
* @returns
*/
export const KidsTranslationsContainer = ({ phrase, imageUrl, id }: KidsTranslationContainerProps): JSX.Element => {
};

const KidsTranslationsContainer = ({ phrase, imageUrl, id }: KidsTranslationContainerProps): JSX.Element => {
const { currentLanguage, otherLanguage } = useLanguage();
const { t } = useTranslation();

const currentTranslation = phrase.getTranslation(currentLanguage);
const otherTranslation = phrase.getTranslation(otherLanguage);
const renderFor = usePlatform();
const isPlaying = useAtomValue(dictionaryAudioPlayAtom);
const activePhrase = useAtomValue(dictionaryActivePhraseAtom);

const ACTIVE_STYLE = {
[currentLanguage]: 'shadow-czech transform rotate-[-5deg] !bg-kiosk-red',
[otherLanguage]: 'shadow-ukraine transform rotate-[5deg] !bg-kiosk-yellow',
};

const isActiveLanguage = (language: Language) =>
isPlaying && activePhrase === phrase.getTranslation(language) && renderFor === Platform.KIOSK;

const isActive = {
[currentLanguage]: isActiveLanguage(currentLanguage),
[otherLanguage]: isActiveLanguage(otherLanguage),
};

const cardClasses = [
'max-w-sm',
'rounded-2xl',
'overflow-hidden',
'shadow-xl',
'm-5',
'md:m-8',
'bg-[#f7e06a]',
renderFor === Platform.KIOSK ? 'w-[400px]' : 'w-72 max-h-[34rem]',
isActive[currentLanguage] ? ACTIVE_STYLE[currentLanguage] : '',
isActive[otherLanguage] ? ACTIVE_STYLE[otherLanguage] : '',
].join(' ');

const renderKidsTranslation = (language: Language, isActive: boolean) => (
<KidsTranslation
language={language}
transcription={phrase.getTranscription(language)}
translation={phrase.getTranslation(language)}
soundUrl={phrase.getSoundUrl(language)}
isActive={isActive}
/>
);

return (
<div className="max-w-sm rounded-2xl overflow-hidden shadow-xl w-72 m-5 md:m-8 bg-[#f7e06a] max-h-[34rem]">
<button
className="w-72 h-72 relative bg-white"
onClick={() => AudioPlayer.getInstance().playSrc(phrase.getSoundUrl(otherLanguage))}
aria-label={t('utils.play') + ' ' + otherTranslation}
>
<Image id={id} src={imageUrl ?? ''} layout="fill" sizes="20vw" objectFit="cover" alt={phrase.getTranslation(otherLanguage)} />
</button>
<div className="px-6 py-4">
<KidsTranslation
language={currentLanguage}
transcription={phrase.getTranscription(currentLanguage)}
translation={currentTranslation}
soundUrl={phrase.getSoundUrl(currentLanguage)}
/>
<KidsTranslation
language={otherLanguage}
transcription={phrase.getTranscription(otherLanguage)}
translation={otherTranslation}
soundUrl={phrase.getSoundUrl(otherLanguage)}
<div className={cardClasses} style={{ position: 'relative' }}>
{(isActive[currentLanguage] || isActive[otherLanguage]) && (
<div
className={`absolute top-0 bottom-0 left-0 right-0 z-10 ${isActive[currentLanguage] ? currentLanguage : ''} ${
isActive[otherLanguage] ? otherLanguage : ''
}`}
/>
)}
<KioskDictionaryCardImage
phrase={phrase}
imageUrl={imageUrl}
id={id}
isActive={isActive[currentLanguage] || isActive[otherLanguage]}
/>
<div className={renderFor === Platform.KIOSK ? 'flex flex-row justify-between' : 'px-6 py-4'}>
{isActive[currentLanguage] && renderKidsTranslation(currentLanguage, true)}
{isActive[otherLanguage] && renderKidsTranslation(otherLanguage, true)}
{!isActive[currentLanguage] && !isActive[otherLanguage] && (
<>
{/* Not playing any sound, show both buttons */}
{renderKidsTranslation(currentLanguage, false)}
{renderKidsTranslation(otherLanguage, false)}
</>
)}
</div>
</div>
);
};

export default KidsTranslationsContainer;
40 changes: 40 additions & 0 deletions components/basecomponents/Kiosk/CountdownCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const CountdownCircle = ({ countdown, radius, countdownTime }: { countdown: number; radius: number; countdownTime: number }) => {
// Calculate progress percentage and create a gradient based on this
const baseCircleColor = '#013abd33';
const progressPercentage = (countdown / (countdownTime / 1000)) * 100;
const gradientColor = progressPercentage > 0 ? `#FAD741` : baseCircleColor;
const circumference = 2 * Math.PI * radius;
const progress = circumference - (progressPercentage / 100) * circumference;
return (
<div>
<svg width="128" height="128" viewBox="0 0 128 128">
<circle cx="60" cy="60" r={radius} stroke={baseCircleColor} strokeWidth="6" fill="transparent" />
<circle
cx="60"
cy="60"
r={radius}
stroke={gradientColor}
strokeWidth="6"
fill="transparent"
strokeDasharray={circumference}
strokeDashoffset={progress}
/>
<text
x="48%"
y="50%"
dominant-baseline="middle"
text-anchor="middle"
style={{
fontWeight: '700',
fontSize: '42px',
lineHeight: '49px',
fill: '#023ABD',
}}
>
{countdown}
</text>
</svg>
</div>
);
};
export default CountdownCircle;
22 changes: 22 additions & 0 deletions components/basecomponents/Kiosk/GameTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Image from 'next/image';
import Link from 'next/link';

import { convertToSlug } from '../../../utils/stringHelpers';

type GameTileProps = {
name: string;
image: string;
title?: string;
};
const GameTile = ({ name, image, title }: GameTileProps) => {
return (
<Link href={`/kiosk/game/${convertToSlug(name)}`}>
<div className="flex flex-col px-[71px] py-[44px] justify-center items-center p-6 bg-white rounded-[60px] drop-shadow-lg">
<Image src={`/images/kiosk/${image}`} width={220} height={220} alt="Movapp logo" className="mb-4" />
<span className="font-bold text-3xl leading-9 flex items-center text-center text-blue-700">{title}</span>
</div>
</Link>
);
};

export default GameTile;
Loading

0 comments on commit bb944df

Please sign in to comment.