feat: better session display
All checks were successful
Deploy Website / deploy (push) Successful in 1m21s
Lint Codebase / lint (push) Successful in 57s

This commit is contained in:
TheClashFruit 2024-08-30 16:05:54 +02:00
parent 9bb6362a68
commit b478e46055
Signed by: TheClashFruit
GPG key ID: 09BB24C34C2F3204
5 changed files with 150 additions and 45 deletions

View file

@ -20,13 +20,15 @@
"next": "14.2.6", "next": "14.2.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"showdown": "^2.1.0" "showdown": "^2.1.0",
"ua-parser-js": "^1.0.38"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.16.2", "@types/node": "^20.16.2",
"@types/react": "^18.3.4", "@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/ua-parser-js": "^0.7.39",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-next": "14.2.6", "eslint-config-next": "14.2.6",
"sass": "^1.77.8", "sass": "^1.77.8",

View file

@ -9,17 +9,20 @@ import { useEffect, useState } from 'react';
import cookie from 'cookie'; import cookie from 'cookie';
import Meta from '@/components/Meta'; import Meta from '@/components/Meta';
import { UAParser } from 'ua-parser-js';
import styles from '@/styles/Settings.module.scss';
import { X } from 'lucide-react';
interface SettingsType { interface SettingsType {
animations: boolean; animations: boolean;
ads: boolean; ads: boolean;
language: string;
} }
export default function Settings({ sessions }: { sessions: any[] }) { export default function Settings({ sessions }: { sessions: any[] }) {
const [ settings, setSettings ] = useState<SettingsType>({ const [ settings, setSettings ] = useState<SettingsType>({
animations: true, animations: true,
ads: true, ads: true
language: 'en'
}); });
useEffect(() => { useEffect(() => {
@ -34,6 +37,9 @@ export default function Settings({ sessions }: { sessions: any[] }) {
localStorage.setItem('crss_settings', JSON.stringify(settings)); localStorage.setItem('crss_settings', JSON.stringify(settings));
}, [ settings ]); }, [ settings ]);
const currentSession = sessions.find(s => s.current);
const otherSessions = sessions.filter(s => !s.current);
return ( return (
<> <>
<Meta page={{ title: 'Settings' }} /> <Meta page={{ title: 'Settings' }} />
@ -42,54 +48,52 @@ export default function Settings({ sessions }: { sessions: any[] }) {
<PageContent> <PageContent>
<h1>Settings</h1> <h1>Settings</h1>
<div style={{ display: 'flex', flexDirection: 'column', gap: '.5rem' }}> { /* TODO: Move it to the css 😒 */ }
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '.5rem', marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label htmlFor="ani">Animations</label> <label htmlFor="ani">Animations</label>
<input id="ani" type="checkbox" checked={settings.animations} onChange={e => setSettings({ ...settings, animations: e.target.checked })} /> <input id="ani" type="checkbox" checked={settings.animations} onChange={e => setSettings({ ...settings, animations: e.target.checked })} />
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<label htmlFor="ads">Ads</label> <label htmlFor="ads">Ads</label>
<input id="ads" type="checkbox" checked={settings.ads} onChange={e => setSettings({ ...settings, ads: e.target.checked })} /> <input id="ads" type="checkbox" checked={settings.ads} onChange={e => setSettings({ ...settings, ads: e.target.checked })} />
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<label htmlFor="lang">Language</label>
<select id="lang" onChange={e => setSettings({ ...settings, language: e.target.value })}>
<option value="en">English</option>
<option value="hu">Hungarian</option>
</select>
</div>
</div> </div>
<h2>Sessions</h2> <h2>Sessions</h2>
<table> <p>
<thead> View and manage your active sessions.
<tr> </p>
<th>User Agent</th>
<th>Created</th> <h3>Current Session</h3>
<th>Expires</th>
<th>Actions</th> <div className={styles.sessions}>
</tr> <div className={styles.sessionCard}>
</thead> <div>
<tbody> <p>{currentSession.user_agent.os.name} {currentSession.user_agent.os.version} &bull; {currentSession.user_agent.browser.name} {currentSession.user_agent.browser.version}</p>
{sessions.map(session => ( <p>{new Date(currentSession.created).toLocaleString('en-GB')}</p>
<tr key={session.id}> </div>
<td><code>{session.user_agent}</code></td> </div>
<td>{new Date(session.created).toLocaleDateString('hu-HU')}</td> </div>
<td>{new Date(session.expires).toLocaleDateString('hu-HU')}</td>
<td> <h3>Other Sessions</h3>
{session.current ? (
<label>Current Session</label> <div className={styles.sessions}>
) : ( {otherSessions.map(session => (
<button disabled>Sign Out</button> <div key={session.id} className={styles.sessionCard}>
)} <div>
</td> <p>{session.user_agent.os.name} {session.user_agent.os.version} &bull; {session.user_agent.browser.name} {session.user_agent.browser.version}</p>
</tr> <p>{new Date(session.created).toLocaleString('en-GB')}</p>
))} </div>
</tbody>
</table> <button onClick={() => { alert('Not yet implemented, yeah...'); }}>
<X />
</button>
</div>
))}
</div>
</PageContent> </PageContent>
<Footer /> <Footer />
@ -123,8 +127,36 @@ export async function getServerSideProps(context: any) {
sessions.forEach((s: any) => { sessions.forEach((s: any) => {
s.current = session.id === s.id; s.current = session.id === s.id;
s.created = s.created.toISOString(); s.created = s.created.toISOString();
s.expires = s.expires.toISOString(); s.expires = s.expires.toISOString();
const ua = new UAParser(s.user_agent).getResult();
s.user_agent = {
ua: ua.ua,
browser: {
name: ua.browser.name || null,
version: ua.browser.version || null,
major: ua.browser.major || null
},
engine: {
name: ua.engine.name || null,
version: ua.engine.version || null
},
os: {
name: ua.os.name || null,
version: ua.os.version || null
},
device: {
vendor: ua.device.vendor || null,
model: ua.device.model || null,
type: ua.device.type || null
},
cpu: {
architecture: ua.cpu.architecture || null
}
};
}); });
return { return {

View file

@ -44,6 +44,9 @@ importers:
showdown: showdown:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
ua-parser-js:
specifier: ^1.0.38
version: 1.0.38
devDependencies: devDependencies:
'@types/cookie': '@types/cookie':
specifier: ^0.6.0 specifier: ^0.6.0
@ -57,6 +60,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^18.3.0 specifier: ^18.3.0
version: 18.3.0 version: 18.3.0
'@types/ua-parser-js':
specifier: ^0.7.39
version: 0.7.39
eslint: eslint:
specifier: ^8.57.0 specifier: ^8.57.0
version: 8.57.0 version: 8.57.0
@ -959,6 +965,9 @@ packages:
'@types/react@18.3.4': '@types/react@18.3.4':
resolution: {integrity: sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==} resolution: {integrity: sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==}
'@types/ua-parser-js@0.7.39':
resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==}
'@typescript-eslint/parser@7.2.0': '@typescript-eslint/parser@7.2.0':
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
engines: {node: ^16.0.0 || >=18.0.0} engines: {node: ^16.0.0 || >=18.0.0}
@ -2523,6 +2532,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
ua-parser-js@1.0.38:
resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==}
unbox-primitive@1.0.2: unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@ -3710,6 +3722,8 @@ snapshots:
'@types/prop-types': 15.7.12 '@types/prop-types': 15.7.12
csstype: 3.1.3 csstype: 3.1.3
'@types/ua-parser-js@0.7.39': {}
'@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4)': '@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/scope-manager': 7.2.0
@ -4303,7 +4317,7 @@ snapshots:
eslint: 8.57.0 eslint: 8.57.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0)
eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0)
@ -4334,7 +4348,7 @@ snapshots:
is-bun-module: 1.1.0 is-bun-module: 1.1.0
is-glob: 4.0.3 is-glob: 4.0.3
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@typescript-eslint/parser' - '@typescript-eslint/parser'
- eslint-import-resolver-node - eslint-import-resolver-node
@ -4352,7 +4366,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies: dependencies:
array-includes: 3.1.8 array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5 array.prototype.findlastindex: 1.2.5
@ -5519,6 +5533,8 @@ snapshots:
typescript@5.5.4: {} typescript@5.5.4: {}
ua-parser-js@1.0.38: {}
unbox-primitive@1.0.2: unbox-primitive@1.0.2:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7

View file

@ -0,0 +1,55 @@
@import 'variables.module';
@import 'global.module';
.sessions {
display: flex;
flex-direction: column;
gap: .5rem;
> .sessionCard {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
p {
margin: 0;
}
button {
display: flex;
justify-content: center;
align-items: center;
background: transparent;
color: $colorTextLight1;
border: none;
border-radius: 100%;
padding: 8px;
cursor: pointer;
transition: 150ms;
&:hover {
background: rgba($colorPrimary, 0.35);
color: $colorPrimary;
}
@media (prefers-color-scheme: dark) {
color: $colorTextDark1;
}
}
}
&:not(:last-child) {
margin-bottom: 1rem;
}
}

View file

@ -11,7 +11,7 @@ code, pre, kbd {
$headers: ( $headers: (
h1: 1.95rem, h1: 1.95rem,
h2: 1.5rem, h2: 1.5rem,
h3: 160%, h3: 1.25rem,
h4: 140%, h4: 140%,
h5: 120%, h5: 120%,
h6: 110% h6: 110%