Commit 5a0f7d55 authored by KIEFFER NOE's avatar KIEFFER NOE
Browse files

Merge branch 'transitions' into 'master'

Starting to polish the UI

See merge request le-jeu/t432_las21_t3_b!30
parents 82dda924 6fbbde4c
Pipeline #68742 passed with stages
in 8 minutes and 41 seconds
......@@ -17,6 +17,7 @@
"react-router": "^6.1.1",
"react-router-dom": "^6.1.1",
"react-scripts": "4.0.3",
"react-transition-group": "^4.4.2",
"redux": "^4.1.2",
"sass": "^1.43.4",
"typescript": "^4.5.4",
......@@ -77,6 +78,7 @@
"min_height": 720
},
"devDependencies": {
"@types/react-transition-group": "^4.4.4",
"husky": ">=6",
"lint-staged": ">=10",
"prettier": "^2.5.1",
......
......@@ -2,6 +2,7 @@ import React, { FC } from 'react';
import { Route, Routes } from 'react-router-dom';
import { End } from './pages/End';
import { Game } from './pages/Game';
import { Intro } from './pages/Intro';
import { Menu } from './pages/Menu';
import { Start } from './pages/Start';
import './styles/App.scss';
......@@ -14,7 +15,8 @@ export const App: FC = () => {
return (
<div className="App">
<Routes>
<Route path="/" element={<Menu />} />
<Route path="" element={<Intro />} />
<Route path="menu" element={<Menu />} />
<Route path="start" element={<Start />} />
<Route path="game" element={<Game />} />
<Route path="end" element={<End />} />
......
import React, { FC, useState } from 'react';
import { Transition } from 'react-transition-group';
import { CardSelection } from '../models/PlayableCard';
import { Button } from './Button';
import { Board } from './card/Board';
import { BottomBar, BottomBarCenter, BottomBarContainer, BottomBarLeft, BottomBarRight } from './containers/BottomBar';
import { PlayContainer, PlayContainerBot, PlayContainerMid, PlayContainerTop } from './containers/PlayContainer';
import { PlayContainer, PlayContainerBot, PlayContainerTop } from './containers/PlayContainer';
import { Progress, TopBar } from './containers/TopBar';
export type MultistageCardSelectProp = {
......@@ -13,8 +14,31 @@ export type MultistageCardSelectProp = {
};
const MultistageCardSelect: FC<MultistageCardSelectProp> = ({ cards, onValidate, onChange }: MultistageCardSelectProp) => {
const duration = 100;
const defaultStyle: React.CSSProperties = {
transition: `transform ${duration}ms ease-in-out`,
transform: 'translateX(0)'
};
const transitionStyles: { [index: string]: React.CSSProperties } = {
entering: { transform: 'translateX(100vw)', transition: `transform 0ms ease-in-out` },
entered: { transform: 'translateX(0)' },
exiting: { transform: 'translateX(0)' },
exited: { transform: 'translateX(-100vw)' }
};
const backTransitionStyles: { [index: string]: React.CSSProperties } = {
entering: { transform: 'translateX(-100vw)', transition: `transform 0ms ease-in-out` },
entered: { transform: 'translateX(0)' },
exiting: { transform: 'translateX(0)' },
exited: { transform: 'translateX(100vw)' }
};
const [currentPage, setCurrentPage] = useState<number>(0);
const [selection, setSelection] = useState<{ [index: number]: number[] }>({});
const [show, setShow] = useState(true);
const [isBack, setBack] = useState(false);
const selectionChanged = (ids: number[]) => {
const sel = selection;
......@@ -26,17 +50,27 @@ const MultistageCardSelect: FC<MultistageCardSelectProp> = ({ cards, onValidate,
};
const nextPage = () => {
if (currentPage < cards.length - 1) {
setCurrentPage(currentPage + 1);
} else if (currentPage === cards.length - 1 && onValidate !== undefined) {
onValidate(Object.values(selection).flat());
}
setBack(false);
setShow(false);
setTimeout(() => {
if (currentPage < cards.length - 1) {
setCurrentPage(currentPage + 1);
setShow(true);
} else if (currentPage === cards.length - 1 && onValidate !== undefined) {
onValidate(Object.values(selection).flat());
}
}, duration * 2);
};
const previousPage = () => {
if (currentPage > 0) {
setCurrentPage(currentPage - 1);
}
setBack(true);
setShow(false);
setTimeout(() => {
if (currentPage > 0) {
setCurrentPage(currentPage - 1);
}
setShow(true);
}, duration * 2);
};
return (
......@@ -46,14 +80,24 @@ const MultistageCardSelect: FC<MultistageCardSelectProp> = ({ cards, onValidate,
<Progress percent={(currentPage / (cards.length - 1)) * 100} />
</TopBar>
</PlayContainerTop>
<PlayContainerMid>
<Board
cardsCount={cards[currentPage].count}
cards={cards[currentPage].cards}
selected={selection[cards[currentPage].id] ?? []}
onSelectionChange={selectionChanged}
/>
</PlayContainerMid>
<Transition timeout={duration} in={show}>
{(state) => (
<div
className="component__playcontainer__mid"
style={{
...defaultStyle,
...(isBack ? backTransitionStyles : transitionStyles)[state]
}}
>
<Board
cardsCount={cards[currentPage].count}
cards={cards[currentPage].cards}
selected={selection[cards[currentPage].id] ?? []}
onSelectionChange={selectionChanged}
/>
</div>
)}
</Transition>
<PlayContainerBot>
<BottomBar>
<BottomBarContainer>
......
import React, { FC } from 'react';
import { hslToRgb } from '../models/colors';
import { Run } from '../store/reducer';
/**
* Convertie un score vers une couleur
* @param score Score
* @returns Couleur, de rouge à vers
*/
export const scoreToColor: (score: number) => string = (score: number) => {
if (score < -1) return 'rgb(255,0,0)';
if (score > 1) return 'rgb(0,255,0)';
const val = (score + 1) / 2;
const [r, g, b] = hslToRgb((val / 360) * 120, 1, 0.8);
return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;
};
/**
* Propriétés de l'affichage de progression
*/
export interface ProgressionProps {
runs: Run[];
}
/**
* Affichage de la progression
* @param props Propriétés
* @returns Contenue (jsx)
*/
export const Progression: FC<ProgressionProps> = ({ runs }) => {
const width: number = 23.375459 + (runs.length - 1) * 23.63772;
const runCommonStyle: React.SVGProps<SVGRectElement> = {
width: 15.468329,
height: 15.468329,
transform: 'rotate(45)',
fillOpacity: 1,
stroke: '#cfcfcf',
strokeWidth: 1.5,
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: 4,
strokeDasharray: 'none',
strokeOpacity: 1
};
const runFutureCommonStyle: React.SVGProps<SVGRectElement> = {
fillOpacity: 0,
strokeDasharray: '1.5, 4.5'
};
const pathCommonStyle: React.SVGProps<SVGPathElement> = {
fill: 'none',
fillRule: 'evenodd',
stroke: '#cfcfcf',
strokeWidth: 1.5,
strokeLinecap: 'round',
strokeLinejoin: 'miter',
strokeMiterlimit: 4,
strokeDasharray: 'none',
strokeOpacity: 1
};
const runPathCommonStyle: React.SVGProps<SVGPathElement> = {
strokeDasharray: '1.5, 4.5',
strokeDashoffset: 4.2
};
return (
<svg xmlns="http://www.w3.org/2000/svg" style={{ height: '10vh' }} viewBox={`0 0 ${width} 47.542412`}>
<g transform="translate(-8.763942,-9.243386)">
{runs.map((run, index) => {
return (
<rect
key={index}
{...runCommonStyle}
{...(!run.done ? runFutureCommonStyle : {})}
fill={scoreToColor(run.score ?? 0)}
x={21.902107 + Math.floor((index + 1) / 2) * 33.42878}
y={-7.0209241 - Math.floor(index / 2) * 33.4288289}
/>
);
})}
{runs.map((run, index) => {
if (index === 0) {
return <React.Fragment key={index} />;
}
if (index % 2 === 1) {
return (
<path
key={index}
{...pathCommonStyle}
{...(!run.done ? runPathCommonStyle : {})}
d={`M ${25.920552 + Math.floor((index - 1) / 2) * 47.27547},26.929226 ${
38.620508 + Math.floor((index - 1) / 2) * 47.27547
},39.629183`}
/>
);
} else {
return (
<path
key={index}
{...pathCommonStyle}
{...(!run.done ? runPathCommonStyle : {})}
d={`M ${49.558269 + Math.floor((index - 1) / 2) * 47.27547},39.629183 ${
62.258259 + Math.floor((index - 1) / 2) * 47.27547
},26.929226`}
/>
);
}
})}
</g>
</svg>
);
};
......@@ -6,12 +6,10 @@ import { App } from './App';
import { appStore } from './store/store';
ReactDOM.render(
<React.StrictMode>
<Provider store={appStore}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
<Provider store={appStore}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param h The hue
* @param s The saturation
* @param l The lightness
* @return The RGB representation
*/
export const hslToRgb = (h: number, s: number, l: number) => {
let r;
let g;
let b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q2 = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p2 = 2 * l - q2;
r = hue2rgb(p2, q2, h + 1 / 3);
g = hue2rgb(p2, q2, h);
b = hue2rgb(p2, q2, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
};
import React, { FC } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router';
import { Transition } from 'react-transition-group';
import { Button } from '../components/Button';
import { BottomBar, BottomBarContainer, BottomBarRight } from '../components/containers/BottomBar';
import { PlayContainer, PlayContainerBot, PlayContainerMid, PlayContainerTop } from '../components/containers/PlayContainer';
......@@ -19,47 +20,77 @@ export const End: FC = () => {
const runScore = calcScore(effects, reputation);
const dispatch = useDispatch();
const navigate = useNavigate();
const [show, setShow] = useState<boolean>(false);
const duration = 500;
const defaultStyle: React.CSSProperties = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0
};
const transitionStyles: { [index: string]: React.CSSProperties } = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 }
};
const goNext = () => {
dispatch({ type: 'stats/run' });
dispatch({ type: 'stats/run', score: runScore });
dispatch({ type: 'state/reputation', reputation: runScore * (effects.filter((e) => e.name === 'reputation')[0]?.value ?? 0) });
navigate('/start');
};
useEffect(() => {
setTimeout(() => setShow(true), 0);
}, []);
return (
<PlayContainer>
<PlayContainerTop>
<Transition in={show} timeout={duration} onExited={() => goNext()}>
{(state) => (
<div
style={{
fontWeight: 'bold',
fontSize: '24px',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
...defaultStyle,
...transitionStyles[state]
}}
>
Résultats de la Course #{run + 1}
</div>
</PlayContainerTop>
<PlayContainerMid>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ width: '45%' }}>
<StatsDisplay effects={effects} score={runScore} />
</div>
<PlayContainer>
<PlayContainerTop>
<div
style={{
fontWeight: 'bold',
fontSize: '24px',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Résultats de la Course #{run + 1}
</div>
</PlayContainerTop>
<PlayContainerMid>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ width: '45%' }}>
<StatsDisplay effects={effects} score={runScore} />
</div>
</div>
</PlayContainerMid>
<PlayContainerBot>
<BottomBar>
<BottomBarContainer>
<BottomBarRight>
<Button bordered={false} variation="success" onClick={() => setShow(false)}>
Course suivante
</Button>
</BottomBarRight>
</BottomBarContainer>
</BottomBar>
</PlayContainerBot>
</PlayContainer>
</div>
</PlayContainerMid>
<PlayContainerBot>
<BottomBar>
<BottomBarContainer>
<BottomBarRight>
<Button bordered={false} variation="success" onClick={goNext}>
Course suivante
</Button>
</BottomBarRight>
</BottomBarContainer>
</BottomBar>
</PlayContainerBot>
</PlayContainer>
)}
</Transition>
);
};
import React, { FC, useState } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { Transition } from 'react-transition-group';
import MultistageCardSelect from '../components/MultistageCardSelect';
import { StatsDisplay } from '../components/StatsDisplay';
import { calcEffects } from '../models/calc';
......@@ -15,18 +16,46 @@ import { AppState } from '../store/reducer';
* @returns Contenue (jsx)
*/
export const Game: FC = () => {
const duration = 500;
const defaultStyle: React.CSSProperties = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0
};
const transitionStyles: { [index: string]: React.CSSProperties } = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 }
};
const rightDefaultStyle: React.CSSProperties = {
transition: `right ${duration}ms ease-in-out`,
right: '-25vw'
};
const rightTransitionStyles: { [index: string]: React.CSSProperties } = {
entering: { right: '-25vw' },
entered: { right: 0 },
exiting: { right: 0 },
exited: { right: '-25vw' }
};
const stats = useSelector((state: AppState) => state.stats);
const [shownEffects, setShownEffects] = useState<EndEffect[]>(calcEffects([], Cards));
const [score, setScore] = useState<number>(calcScore(calcEffects([], Cards), stats.reputation));
const dispatch = useDispatch();
const navigate = useNavigate();
const [show, setShow] = useState<boolean>(false);
const [showRight, setShowRight] = useState<boolean>(false);
const onValidate = (val: number[]) => {
dispatch({
type: 'game/end',
effects: calcEffects(val, Cards)
});
navigate('/end');
setShowRight(false);
};
const onChange = (val: number[]) => {
......@@ -35,12 +64,35 @@ export const Game: FC = () => {
setScore(calcScore(effects, stats.reputation));
};
useEffect(() => {
setTimeout(() => setShow(true), 0);
}, []);
return (
<>
<MultistageCardSelect cards={filterCards(Cards, stats)} onValidate={onValidate} onChange={onChange} />
<div className="stats__right">
<StatsDisplay effects={shownEffects} score={score} />
</div>
</>
<Transition in={show} timeout={duration} onEntered={() => setShowRight(true)} onExited={() => navigate('/end')}>
{(backState) => (
<div
style={{
...defaultStyle,
...transitionStyles[backState]
}}
>
<MultistageCardSelect cards={filterCards(Cards, stats)} onValidate={onValidate} onChange={onChange} />
<Transition in={showRight} timeout={duration} onExited={() => setShow(false)}>
{(state) => (
<div
className="stats__right"
style={{
...rightDefaultStyle,
...rightTransitionStyles[state]
}}
>
<StatsDisplay effects={shownEffects} score={score} />
</div>
)}
</Transition>
</div>
)}
</Transition>
);
};
import React, { FC, useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import { Transition } from 'react-transition-group';
export const Intro: FC = () => {
const duration = 1000;
const defaultStyle: React.CSSProperties = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
height: '100vh',
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0
};
const transitionStyles: { [index: string]: React.CSSProperties } = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 }
};
const navigate = useNavigate();
const [show, setShow] = useState<boolean>(false);
useEffect(() => {
setTimeout(() => setShow(true), 0);
setTimeout(() => setShow(false), 2500);
}, []);
return (
<Transition
in={show}
timeout={duration}
onExited={() => {
navigate('/menu');
}}
>
{(state) => (
<div
style={{
...defaultStyle,
...transitionStyles[state]
}}
>
<div style={{ fontSize: '72px' }}>Le Jeu™</div>
</div>
)}
</Transition>
);
};
import React, { FC } from 'react';
import React, { FC, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';