privacy policy and graph layout
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { faShield } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as React from 'react'
|
||||
|
||||
const PrivacyPolicy: React.FC = () => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div className="privacy" onClick={handleOpen} title="Privacy Policy & Impressum">
|
||||
<FontAwesomeIcon icon={faShield} />
|
||||
<p className="p1">legal</p>
|
||||
<p className="p2">notes</p>
|
||||
</div>
|
||||
{open?
|
||||
<div className="modal-wrapper">
|
||||
<div className="modal-backdrop" onClick={handleClose} />
|
||||
<div className="modal">
|
||||
<div className="codicon codicon-close modal-close" onClick={handleClose}></div>
|
||||
<h2>Privacy Policy & Impressum</h2>
|
||||
|
||||
<p>Our server collects metadata (such as IP address, browser, operating system)
|
||||
and the data that the user enters into the editor. The data is used to
|
||||
compute the Lean output and display it to the user. The information will be stored
|
||||
as long as the user stays on our website and will be deleted immediately afterwards.
|
||||
We keep logs to improve our software, but the contained data is anonymized.</p>
|
||||
|
||||
<p>We do not use cookies, but your game progress is stored in the browser
|
||||
as site data. Your game progress is not saved on the server; if you delete
|
||||
your browser storage, it is completely gone.
|
||||
</p>
|
||||
|
||||
<p>Our server is located in Germany.</p>
|
||||
|
||||
<p><strong>Contact information:</strong><br />
|
||||
Jon Eugster<br />
|
||||
Mathematisches Institut der Heinrich-Heine-Universität Düsseldorf<br />
|
||||
Universitätsstr. 1<br />
|
||||
40225 Düsseldorf<br />
|
||||
Germany<br />
|
||||
<a href="mailto:jon.eugster@hhu.de">jon.eugster@hhu.de</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</div> : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivacyPolicy
|
||||
@@ -5,7 +5,9 @@ import cytoscape, { LayoutOptions } from 'cytoscape'
|
||||
import klay from 'cytoscape-klay';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Split from 'react-split'
|
||||
|
||||
import PrivacyPolicy from './PrivacyPolicy';
|
||||
|
||||
cytoscape.use( klay );
|
||||
|
||||
@@ -16,14 +18,23 @@ import Markdown from './Markdown';
|
||||
import { selectCompleted } from '../state/progress';
|
||||
import { GameIdContext } from '../App';
|
||||
|
||||
const N = 24 // max number of levels per world
|
||||
const R = 800 // radius of a world
|
||||
const r = 110 // radius of a level
|
||||
const s = 100 // global scale
|
||||
const padding = 2000 // padding of the graphic (on a different scale)
|
||||
|
||||
function LevelIcon({ worldId, levelId, position }) {
|
||||
const gameId = React.useContext(GameIdContext)
|
||||
const completed = useSelector(selectCompleted(gameId, worldId,levelId))
|
||||
|
||||
const x = s * position.x + Math.sin(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2*Math.floor((levelId - 1)/N))
|
||||
const y = s * position.y - Math.cos(levelId * 2 * Math.PI / N) * (R + 1.2*r + 2*Math.floor((levelId - 1)/N))
|
||||
|
||||
// TODO: relative positioning?
|
||||
return (
|
||||
<Link to={`/game/${gameId}/world/${worldId}/level/${levelId}`} key={`/game/${gameId}/world/${worldId}/level/${levelId}`}>
|
||||
<circle fill={completed ? "green" :"#aaa"} cx={position.x + Math.sin(levelId/5) * 9} cy={position.y - Math.cos(levelId/5) * 9} r="0.8" />
|
||||
<circle fill={completed ? "green" :"#999"} cx={x} cy={y} r={r} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -42,56 +53,66 @@ function Welcome() {
|
||||
}
|
||||
}, [gameInfo.data?.title])
|
||||
|
||||
const padding = 20
|
||||
|
||||
const svgElements = []
|
||||
|
||||
if (gameInfo.data) {
|
||||
for (let i in gameInfo.data.worlds.edges) {
|
||||
const edge = gameInfo.data.worlds.edges[i]
|
||||
svgElements.push(
|
||||
<line key={`pathway${i}`} x1={nodes[edge[0]].position.x} y1={nodes[edge[0]].position.y}
|
||||
x2={nodes[edge[1]].position.x} y2={nodes[edge[1]].position.y} stroke="#1976d2" strokeWidth="1"/>
|
||||
<line key={`pathway${i}`} x1={s*nodes[edge[0]].position.x} y1={s*nodes[edge[0]].position.y}
|
||||
x2={s*nodes[edge[1]].position.x} y2={s*nodes[edge[1]].position.y} stroke="#1976d2" strokeWidth={s}/>
|
||||
)
|
||||
}
|
||||
|
||||
for (let id in nodes) {
|
||||
let position: cytoscape.Position = nodes[id].position
|
||||
|
||||
svgElements.push(
|
||||
<Link key={`world${id}`} to={`/game/${gameId}/world/${id}/level/0`}>
|
||||
<circle className="world-circle" cx={position.x} cy={position.y} r="8" />
|
||||
<text className="world-name"
|
||||
x={position.x} y={position.y}>{nodes[id].data.title ? nodes[id].data.title : id}</text>
|
||||
</Link>
|
||||
)
|
||||
|
||||
for (let i = 1; i <= gameInfo.data.worldSize[id]; i++) {
|
||||
svgElements.push(
|
||||
<LevelIcon position={position} worldId={id} levelId={i} />
|
||||
)
|
||||
}
|
||||
|
||||
svgElements.push(
|
||||
<Link key={`world${id}`} to={`/game/${gameId}/world/${id}/level/0`}>
|
||||
<circle className="world-circle" cx={s*position.x} cy={s*position.y} r={R}
|
||||
fill="#1976d2"/>
|
||||
<foreignObject className="world-title-wrapper" x={s*position.x} y={s*position.y}
|
||||
width={1.42*R} height={1.42*R} transform={"translate("+ -.71*R +","+ -.71*R +")"}>
|
||||
<div>
|
||||
<p className="world-title" style={{fontSize: Math.floor(R/4) + "px"}}>
|
||||
{nodes[id].data.title ? nodes[id].data.title : id}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return <div>
|
||||
return <div className="app-content ">
|
||||
{ gameInfo.isLoading?
|
||||
<Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}><CircularProgress /></Box>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
:
|
||||
<div className="app-content">
|
||||
<Box sx={{ m: 3 }}>
|
||||
<Typography variant="body1" component="div">
|
||||
<Split className="welcome" minSize={200} sizes={[70, 30]}>
|
||||
<div className="column">
|
||||
<Typography variant="body1" component="div" className="welcome-text">
|
||||
<Markdown>{gameInfo.data?.introduction}</Markdown>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box textAlign='center' sx={{ m: 5 }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="30%"
|
||||
viewBox={bounds ? `${bounds.x1 - padding} ${bounds.y1 - padding} ${bounds.x2 - bounds.x1 + 2 * padding} ${bounds.y2 - bounds.y1 + 2 * padding}` : ''}>
|
||||
{svgElements}
|
||||
</svg>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Box textAlign='center' sx={{ m: 5 }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox={bounds ? `${s*bounds.x1 - padding} ${s*bounds.y1 - padding} ${s*bounds.x2 - s*bounds.x1 + 2 * padding} ${s*bounds.y2 - s*bounds.y1 + 2 * padding}` : ''}>
|
||||
{svgElements}
|
||||
</svg>
|
||||
</Box>
|
||||
</div>
|
||||
</Split>
|
||||
}
|
||||
|
||||
<PrivacyPolicy/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -119,9 +140,8 @@ function computeWorldLayout(worlds) {
|
||||
headless: true,
|
||||
styleEnabled: false
|
||||
})
|
||||
// TODO: Jon play around with graph layout
|
||||
|
||||
const layout = cy.layout({name: "klay", klay: {direction: "DOWN"}} as LayoutOptions).run()
|
||||
const layout = cy.layout({name: "klay", klay: {direction: "DOWN", nodePlacement: "LINEAR_SEGMENTS"}} as LayoutOptions).run()
|
||||
let nodes = {}
|
||||
cy.nodes().forEach((node, id) => {
|
||||
nodes[node.id()] = {
|
||||
|
||||
@@ -1,12 +1,191 @@
|
||||
|
||||
svg .world-circle {
|
||||
/* svg .world-circle {
|
||||
fill: var(--clr-primary)
|
||||
} */
|
||||
|
||||
.welcome {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
svg .world-name {
|
||||
fill: white;
|
||||
font-size: 2px;
|
||||
font-weight: 500;
|
||||
text-anchor: middle;
|
||||
dominant-baseline: middle;
|
||||
.app-content {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.welcome .column {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: .67em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
h5, h6 {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/***************/
|
||||
/* SVG Graphic */
|
||||
/***************/
|
||||
|
||||
svg .world-title-wrapper {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
svg .world-title-wrapper div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
svg .world-title-wrapper div {
|
||||
display: flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
overflow: visible;
|
||||
|
||||
}
|
||||
|
||||
svg .world-title {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/******************/
|
||||
/* Privacy Button */
|
||||
/******************/
|
||||
|
||||
.privacy {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 25px;
|
||||
border-radius: 20px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
display: flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
color: #aaa;
|
||||
background-color: #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.privacy p {
|
||||
position: absolute;
|
||||
color: #888;
|
||||
bottom: 1.5px;
|
||||
font-size: 6px;
|
||||
}
|
||||
|
||||
.privacy .p1 {
|
||||
transform: rotate(50deg);
|
||||
left: 1.5px;
|
||||
}
|
||||
|
||||
.privacy .p2 {
|
||||
transform: rotate(-50deg);
|
||||
right: 1.5px;
|
||||
}
|
||||
|
||||
/*****************/
|
||||
/* Privacy Popup */
|
||||
/*****************/
|
||||
|
||||
.modal-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
min-width: 50%;
|
||||
max-width: 60ch;
|
||||
background: #fff;
|
||||
z-index: 3;
|
||||
padding: 2em;
|
||||
border-radius: 1em;
|
||||
text-align: left;
|
||||
color: var(--vscode-breadcrumb-foreground);
|
||||
}
|
||||
|
||||
.modal input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal .form-error {
|
||||
color: #a00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal input[type="submit"] {
|
||||
border: none;
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
cursor: pointer;
|
||||
padding: .5em 1em;
|
||||
border-radius: .2em;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
float: right;
|
||||
scale: 2;
|
||||
color: var(--vscode-breadcrumb-focusForeground);
|
||||
}
|
||||
|
||||
.modal table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user