privacy policy and graph layout

pull/54/head
Jon Eugster 3 years ago
parent 6e8911e5da
commit 9eb0f2543f

@ -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 &amp; 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 &amp; 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 klay from 'cytoscape-klay';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Split from 'react-split'
import PrivacyPolicy from './PrivacyPolicy';
cytoscape.use( klay ); cytoscape.use( klay );
@ -16,14 +18,23 @@ import Markdown from './Markdown';
import { selectCompleted } from '../state/progress'; import { selectCompleted } from '../state/progress';
import { GameIdContext } from '../App'; 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 }) { function LevelIcon({ worldId, levelId, position }) {
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const completed = useSelector(selectCompleted(gameId, worldId,levelId)) 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? // TODO: relative positioning?
return ( return (
<Link to={`/game/${gameId}/world/${worldId}/level/${levelId}`} key={`/game/${gameId}/world/${worldId}/level/${levelId}`}> <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> </Link>
) )
} }
@ -42,56 +53,66 @@ function Welcome() {
} }
}, [gameInfo.data?.title]) }, [gameInfo.data?.title])
const padding = 20
const svgElements = [] const svgElements = []
if (gameInfo.data) { if (gameInfo.data) {
for (let i in gameInfo.data.worlds.edges) { for (let i in gameInfo.data.worlds.edges) {
const edge = gameInfo.data.worlds.edges[i] const edge = gameInfo.data.worlds.edges[i]
svgElements.push( svgElements.push(
<line key={`pathway${i}`} x1={nodes[edge[0]].position.x} y1={nodes[edge[0]].position.y} <line key={`pathway${i}`} x1={s*nodes[edge[0]].position.x} y1={s*nodes[edge[0]].position.y}
x2={nodes[edge[1]].position.x} y2={nodes[edge[1]].position.y} stroke="#1976d2" strokeWidth="1"/> x2={s*nodes[edge[1]].position.x} y2={s*nodes[edge[1]].position.y} stroke="#1976d2" strokeWidth={s}/>
) )
} }
for (let id in nodes) { for (let id in nodes) {
let position: cytoscape.Position = nodes[id].position 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++) { for (let i = 1; i <= gameInfo.data.worldSize[id]; i++) {
svgElements.push( svgElements.push(
<LevelIcon position={position} worldId={id} levelId={i} /> <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? { 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"> <Split className="welcome" minSize={200} sizes={[70, 30]}>
<Box sx={{ m: 3 }}> <div className="column">
<Typography variant="body1" component="div"> <Typography variant="body1" component="div" className="welcome-text">
<Markdown>{gameInfo.data?.introduction}</Markdown> <Markdown>{gameInfo.data?.introduction}</Markdown>
</Typography> </Typography>
</Box> </div>
<div className="column">
<Box textAlign='center' sx={{ m: 5 }}> <Box textAlign='center' sx={{ m: 5 }}>
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="30%" <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox={bounds ? `${bounds.x1 - padding} ${bounds.y1 - padding} ${bounds.x2 - bounds.x1 + 2 * padding} ${bounds.y2 - bounds.y1 + 2 * padding}` : ''}> 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} {svgElements}
</svg> </svg>
</Box> </Box>
</div> </div>
</Split>
} }
<PrivacyPolicy/>
</div> </div>
} }
@ -119,9 +140,8 @@ function computeWorldLayout(worlds) {
headless: true, headless: true,
styleEnabled: false 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 = {} let nodes = {}
cy.nodes().forEach((node, id) => { cy.nodes().forEach((node, id) => {
nodes[node.id()] = { nodes[node.id()] = {

@ -1,12 +1,191 @@
svg .world-circle { /* svg .world-circle {
fill: var(--clr-primary) fill: var(--clr-primary)
} */
.welcome {
height: 100%;
flex: 1;
min-height: 0;
display: flex;
}
.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-name { svg .world-title {
fill: white;
font-size: 2px;
font-weight: 500; font-weight: 500;
text-anchor: middle; color: white;
dominant-baseline: middle; 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%;
} }

23
package-lock.json generated

@ -18,6 +18,7 @@
"@types/cytoscape": "^3.19.9", "@types/cytoscape": "^3.19.9",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"cytoscape": "^3.23.0", "cytoscape": "^3.23.0",
"cytoscape-elk": "^2.1.0",
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"express": "^4.18.2", "express": "^4.18.2",
@ -34,6 +35,7 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"vscode-ws-jsonrpc": "^2.0.1", "vscode-ws-jsonrpc": "^2.0.1",
"web-worker": "^1.2.0",
"ws": "^8.11.0" "ws": "^8.11.0"
}, },
"devDependencies": { "devDependencies": {
@ -4180,6 +4182,17 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/cytoscape-elk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cytoscape-elk/-/cytoscape-elk-2.1.0.tgz",
"integrity": "sha512-stkKoUTNOqpyP5eMuqatK0EYir2NWGTH+XlY0rxFj0t0HiQPGI4AuSuTPaGbNM1WhVfb0tWJ5TQQ0R0qshACLw==",
"dependencies": {
"elkjs": "^0.8.1"
},
"peerDependencies": {
"cytoscape": "^3.2.0"
}
},
"node_modules/cytoscape-klay": { "node_modules/cytoscape-klay": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz",
@ -4435,6 +4448,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz",
"integrity": "sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==" "integrity": "sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ=="
}, },
"node_modules/elkjs": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz",
"integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ=="
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -9869,6 +9887,11 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/web-worker": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz",
"integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA=="
},
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.75.0", "version": "5.75.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",

@ -14,6 +14,7 @@
"@types/cytoscape": "^3.19.9", "@types/cytoscape": "^3.19.9",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"cytoscape": "^3.23.0", "cytoscape": "^3.23.0",
"cytoscape-elk": "^2.1.0",
"cytoscape-klay": "^3.1.4", "cytoscape-klay": "^3.1.4",
"debounce": "^1.2.1", "debounce": "^1.2.1",
"express": "^4.18.2", "express": "^4.18.2",
@ -30,6 +31,7 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"vscode-ws-jsonrpc": "^2.0.1", "vscode-ws-jsonrpc": "^2.0.1",
"web-worker": "^1.2.0",
"ws": "^8.11.0" "ws": "^8.11.0"
}, },
"devDependencies": { "devDependencies": {

Loading…
Cancel
Save