Initial commit, working server
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,203 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sykkelaksjon</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-div">
|
||||
<p>Laster data...</p>
|
||||
</div>
|
||||
<div x-data="{ unitType: $store.state.data.activityTypes[0]?.unit ?? 'km' }" id="content-div" style="display: none;">
|
||||
<h1>Velkommen, <span x-text="$store.state.data.name"></span></h1>
|
||||
<div class="outer-vertical-flex">
|
||||
<div class="flex-container">
|
||||
<div class="vertical-flex">
|
||||
<div class="register-activity-div">
|
||||
<h3>Registrer ny aktivitet:</h3>
|
||||
<form id="activity-form">
|
||||
<div class="activity-type-div">
|
||||
<label for="activity-type">Type aktivitet: </label>
|
||||
<template x-if="$store.state.data.activityTypes.length > 1">
|
||||
<select name="activity-type" id="activity-type" x-on:change="unitType = event.target[event.target.selectedIndex].dataset.unit;">
|
||||
<template x-for="activityType in $store.state.data.activityTypes" :key="activityType.id">
|
||||
<option x-text="activityType.name" x-bind:value="activityType.id" x-bind:data-unit="activityType.unit"></option>
|
||||
</template>
|
||||
</select>
|
||||
</template>
|
||||
<template x-if="$store.state.data.activityTypes.length == 1">
|
||||
<span><span x-text="$store.state.data.activityTypes[0].name"></span><input type="hidden" name="activity-type" x-bind:value="$store.state.data.activityTypes[0].id" /></span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="distance-div">
|
||||
<label for="activity-distance">Avstand: </label>
|
||||
<input id="activity-distance" name="activity-distance" type="number" min="0" step="0.1" />
|
||||
<span x-text="unitType"></span>
|
||||
</div>
|
||||
|
||||
<div class="description-div">
|
||||
<label for="activity-description">Beskrivelse:</label>
|
||||
<input type="text" id="activity-description" name="activity-description" />
|
||||
</div>
|
||||
|
||||
<div class="date-div">
|
||||
<label for="activity-date">Dato:</label>
|
||||
<input type="date" id="activity-date" name="activity-date" />
|
||||
</div>
|
||||
<div class="submit-buttons-container">
|
||||
<button id="submit-activity-button">Registrer aktivitet</button>
|
||||
<button id="submit-activity-template-button">Legg til som standardaktivitet</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="registered-activities-div">
|
||||
<h3>Registrerte aktiviteter:</h3>
|
||||
<p>Til sammen: <span x-text="$store.state.data.activities.reduce((p, a) => p + a.numberOfUnits * a.activityType.conversionFactor, 0).toFixed(1)"></span> km</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Dato</th>
|
||||
<th>Beskrivelse</th>
|
||||
<th>Avstand</th>
|
||||
<template x-if="$store.state.data.unitConversionNecessary">
|
||||
<th>Ekvivalent</th>
|
||||
</template>
|
||||
<th>Fjern registrert aktivitet</th>
|
||||
</tr>
|
||||
<template x-for="activity in $store.state.data.activities">
|
||||
<tr>
|
||||
<td x-text="activity.date"></td>
|
||||
<td x-text="activity.description"></td>
|
||||
<td x-text="`${activity.numberOfUnits} ${activity.activityType.unit}`"></td>
|
||||
<template x-if="$store.state.data.unitConversionNecessary">
|
||||
<td x-text="`${activity.numberOfUnits * activity.activityType.conversionFactor} km`"></td>
|
||||
</template>
|
||||
<td><button x-on:click="$store.state.deleteActivity(activity)">Fjern</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="standard-activities-div">
|
||||
<h3>Standardaktiviteter:</h3>
|
||||
<template x-for="activityTemplate in $store.state.data.activityTemplates" :key="activityTemplate.id">
|
||||
<p>
|
||||
<button x-on:click="$store.state.registerTemplateActivity(activityTemplate)">Registrer</button>
|
||||
<span x-text="`${activityTemplate.name}, ${activityTemplate.activityType.name.toLowerCase()}, ${activityTemplate.numberOfUnits} ${activityTemplate.activityType.unit}`"></span>
|
||||
<button x-on:click="$store.state.deleteTemplateActivity(activityTemplate)">Fjern</button>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other-users-div">
|
||||
<h3>Andre aktive:</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Navn</th><th>Total avstand</th><th>Første aktivitet</th><th>Siste aktivitet</th>
|
||||
</tr>
|
||||
<template x-if="$store.state.data.otherUsers.length == 0">
|
||||
<tr>
|
||||
<td colspan="4">Det er ingen andre deltakere</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-if="$store.state.data.otherUsers.length > 0">
|
||||
<template x-for="otherUser in $store.state.data.otherUsers">
|
||||
<tr>
|
||||
<td x-text="otherUser.name"></td>
|
||||
<td x-text="otherUser.totalKilometers"></td>
|
||||
<td x-text="otherUser.earliestActivity"></td>
|
||||
<td x-text="otherUser.latestActivity"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
<template x-if="$store.state.data.isAdmin">
|
||||
<div class="admin-div" >
|
||||
<h3>Administratorinnstillinger:</h3>
|
||||
<details>
|
||||
<summary>Aktivitetstyper</summary>
|
||||
<h4>Opprett ny aktivitetstype:</h4>
|
||||
<form id="activity-type-form" onsubmit="return false;">
|
||||
<div id="activity-type-name-div">
|
||||
<label for="activity-type-name">Navn:</label>
|
||||
<input type="text" name="activity-type-name" id="activity-type-name" />
|
||||
</div>
|
||||
<div id="activity-type-unit-div">
|
||||
<label for="activity-type-unit">Enhet:</label>
|
||||
<input type="text" name="activity-type-unit" id="activity-type-unit" />
|
||||
</div>
|
||||
<div id="activity-type-conversion-factor-div">
|
||||
<label for="activity-type-conversion-factor">Konverteringsfaktor:</label>
|
||||
<input type="number" name="activity-type-conversion-factor" id="activity-type-conversion-factor" min="0" value="1" />
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="postActivityType();">Legg til</button>
|
||||
</div>
|
||||
</form>
|
||||
<h4>Eksisterende aktivitetstyper:</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Aktivitetstype</th>
|
||||
<th>Enhet</th>
|
||||
<th>Konverteringsfaktor til kilometer</th>
|
||||
<th>Fjern</th>
|
||||
</tr>
|
||||
<template x-for="activityType in $store.state.data.activityTypes">
|
||||
<tr>
|
||||
<td x-text="activityType.name"></td>
|
||||
<td x-text="activityType.unit"></td>
|
||||
<td x-text="activityType.conversionFactor.toString()"></td>
|
||||
<td><button x-on:click="$store.state.deleteActivityType(activityType)">Fjern</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Brukere</summary>
|
||||
<template x-for="user in $store.state.data.otherUsers">
|
||||
<details>
|
||||
<summary x-text="`${user.name} (${user.userName})${user.isAdmin ? ' - Administrator' : ''}`"></summary>
|
||||
<template x-if="user.isAdmin">
|
||||
<button x-on:click="$store.state.removeAdministrator(user.userId)">Fjern administrator</button>
|
||||
</template>
|
||||
<template x-if="!user.isAdmin">
|
||||
<button x-on:click="$store.state.makeAdministrator(user.userId)">Gjør til administrator</button>
|
||||
</template>
|
||||
<button x-on:click="$store.state.deleteUser(user.userId)">Slett bruker</button>
|
||||
<h4>Aktiviteter:</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Dato</th>
|
||||
<th>Beskrivelse</th>
|
||||
<th>Avstand</th>
|
||||
<template x-if="$store.state.data.unitConversionNecessary">
|
||||
<th>Ekvivalent</th>
|
||||
</template>
|
||||
<th>Fjern registrert aktivitet</th>
|
||||
</tr>
|
||||
<template x-for="activity in user.activities">
|
||||
<tr>
|
||||
<td x-text="activity.date"></td>
|
||||
<td x-text="activity.description"></td>
|
||||
<td x-text="`${activity.numberOfUnits} ${activity.activityType.unit}`"></td>
|
||||
<template x-if="$store.state.data.unitConversionNecessary">
|
||||
<td x-text="`${activity.numberOfUnits * activity.activityType.conversionFactor} km`"></td>
|
||||
</template>
|
||||
<td><button x-on:click="$store.state.deleteActivity(activity)">Fjern</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</details>
|
||||
</template>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1124
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"transpile": "json2ts --input ../messages/server-message.schema.json --output src/message.d.ts",
|
||||
"transpile-2": "json2ts --input ../messages/openid.schema.json --output src/openid.d.ts",
|
||||
"dev": "npm run transpile && npm run transpile-2 && vite",
|
||||
"build": "npm run transpile && npm run transpile-2 && tsc && vite build",
|
||||
"preview": "npm run transpile && npm run transpile-2 && vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/alpinejs": "^3.13.11",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.15.11",
|
||||
"openid-client": "^6.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="256px" height="256px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
␍<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
␍<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
␍<g id="SVGRepo_iconCarrier"> <path d="M11.5 3C12.3284 3 13 2.32843 13 1.5C13 0.671573 12.3284 0 11.5 0C10.6716 0 10 0.671573 10 1.5C10 2.32843 10.6716 3 11.5 3Z" fill="#000000"/> <path d="M0 12C0 10.4697 1.14578 9.20702 2.62626 9.02304L5.00001 10.6055V14.2361C4.46925 14.7111 3.76836 15 3 15C1.34315 15 0 13.6568 0 12Z" fill="#000000"/> <path d="M11 14.2361C11.5308 14.7111 12.2316 15 13 15C14.6569 15 16 13.6569 16 12C16 10.3431 14.6569 9 13 9C12.2316 9 11.5308 9.28885 11 9.76389V14.2361Z" fill="#000000"/> <path d="M8.28111 5.01444C8.29524 5.00503 8.31184 5 8.32881 5C8.36139 5 8.39117 5.0184 8.40574 5.04754L9.38197 7H13V5H10.618L10.1946 4.15311C9.84124 3.44641 9.11893 3 8.32881 3C7.91699 3 7.51437 3.1219 7.17171 3.35034L4.86132 4.8906C4.32322 5.24934 4 5.85327 4 6.5C4 7.14673 4.32322 7.75066 4.86132 8.1094L7 9.53518V13H9V8.46482L6.05278 6.5L8.28111 5.01444Z" fill="#000000"/> </g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { Alpine as AlpineType } from 'alpinejs'
|
||||
|
||||
declare global {
|
||||
var Alpine: AlpineType
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import type { Activity, ActivityTemplate, ActivityType, Sykkelaksjon } from './message';
|
||||
import type { SykkelaksjonOpenid } from './openid';
|
||||
import './style.css'
|
||||
import Alpine from 'alpinejs'
|
||||
import * as openidClient from 'openid-client';
|
||||
|
||||
// ---------------- HTML ELEMENT SETUP ----------------
|
||||
// Set the default date to today
|
||||
let datepicker = document.getElementById("activity-date") as HTMLInputElement;
|
||||
datepicker.valueAsDate = new Date();
|
||||
|
||||
|
||||
// Override what the activity form does
|
||||
let activityForm = document.getElementById("activity-form") as HTMLFormElement;
|
||||
activityForm.onsubmit = () => false;
|
||||
|
||||
// Override what the activity button does
|
||||
document.getElementById("submit-activity-button")?.addEventListener("click", () => submitFormToUrl(apiUrl + "/submitActivity"));
|
||||
document.getElementById("submit-activity-template-button")?.addEventListener("click", () => submitFormToUrl(apiUrl + "/submitActivityTemplate"));
|
||||
|
||||
let loadingDiv = document.getElementById("loading-div");
|
||||
let contentDiv = document.getElementById("content-div");
|
||||
|
||||
// ---------------- ALPINE STATE -----------------------
|
||||
interface AlpineState {
|
||||
data: Sykkelaksjon,
|
||||
registerTemplateActivity: (template: ActivityTemplate) => void,
|
||||
deleteTemplateActivity: (template: ActivityTemplate) => void,
|
||||
deleteActivity: (activity: Activity) => void,
|
||||
deleteActivityType: (activityType: ActivityType) => void,
|
||||
|
||||
makeAdministrator: (userId: number) => void,
|
||||
removeAdministrator: (userId: number) => void,
|
||||
deleteUser: (userId: number) => void,
|
||||
|
||||
init: () => void,
|
||||
setServerMessage: (newMessage: Sykkelaksjon) => void,
|
||||
getServerMessage: () => Sykkelaksjon,
|
||||
};
|
||||
|
||||
let defaultData: Sykkelaksjon = {
|
||||
name: "ukjent bruker",
|
||||
isAdmin: false,
|
||||
unitConversionNecessary: false,
|
||||
activityTypes: [],
|
||||
activityTemplates: [],
|
||||
activities: [],
|
||||
otherUsers: []
|
||||
}
|
||||
|
||||
let alpineState: AlpineState = {
|
||||
data: defaultData,
|
||||
registerTemplateActivity: registerTemplateActivity,
|
||||
deleteTemplateActivity: deleteTemplateActivity,
|
||||
deleteActivity: deleteActivity,
|
||||
deleteActivityType: deleteActivityType,
|
||||
|
||||
makeAdministrator: makeAdministrator,
|
||||
removeAdministrator: removeAdministrator,
|
||||
deleteUser: deleteUser,
|
||||
|
||||
init() {
|
||||
this.data = defaultData;
|
||||
},
|
||||
setServerMessage(newMessage: Sykkelaksjon) {
|
||||
this.data = newMessage;
|
||||
},
|
||||
getServerMessage(): Sykkelaksjon {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
window.Alpine = Alpine
|
||||
Alpine.store('state', alpineState);
|
||||
Alpine.start();
|
||||
|
||||
|
||||
// ------------------- FUNCTIONS TO BE CALLED FROM THE HTML -----------------------
|
||||
function registerTemplateActivity(template: ActivityTemplate): void {
|
||||
let form = new FormData();
|
||||
form.set("activity-type", template.activityType.id?.toString() ?? "");
|
||||
form.set("activity-description", template.name);
|
||||
form.set("activity-distance", template.numberOfUnits.toString());
|
||||
var currentDate = new Date();
|
||||
form.set("activity-date", `${currentDate.getFullYear().toString().padStart(4, '0')}-${(currentDate.getMonth()+1).toString().padStart(2, '0')}-${currentDate.getDate().toString().padStart(2, '0')}`);
|
||||
submitFormToUrl(apiUrl + "/submitActivity", form);
|
||||
}
|
||||
|
||||
function deleteTemplateActivity(template: ActivityTemplate): void {
|
||||
let form = new FormData();
|
||||
form.set("activity-template-id", template.id?.toString() ?? "");
|
||||
submitFormToUrl(apiUrl + "/deleteActivityTemplate", form, "DELETE");
|
||||
}
|
||||
|
||||
function deleteActivity(activity: Activity): void {
|
||||
let form = new FormData();
|
||||
form.set("activity-id", activity.id?.toString() ?? "");
|
||||
submitFormToUrl(apiUrl + "/deleteActivity", form, "DELETE");
|
||||
}
|
||||
|
||||
function postActivityType(): void {
|
||||
let activityTypeForm = document.getElementById("activity-type-form") as HTMLFormElement;
|
||||
let activtyTypeData = new FormData(activityTypeForm);
|
||||
|
||||
submitFormToUrl(apiUrl + "/addActivityType", activtyTypeData);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window { postActivityType: () => void }
|
||||
}
|
||||
window.postActivityType = postActivityType;
|
||||
|
||||
function deleteActivityType(activityType: ActivityType) {
|
||||
let form = new FormData();
|
||||
form.set("activity-type-id", activityType.id?.toString() ?? "");
|
||||
submitFormToUrl(apiUrl + "/deleteActivityType", form, "DELETE");
|
||||
}
|
||||
|
||||
function makeAdministrator(userId: number) {
|
||||
let form = new FormData();
|
||||
form.set("user-id", userId.toString());
|
||||
submitFormToUrl(apiUrl + "/makeAdmin", form, "PUT")
|
||||
}
|
||||
|
||||
function removeAdministrator(userId: number) {
|
||||
let form = new FormData();
|
||||
form.set("user-id", userId.toString());
|
||||
submitFormToUrl(apiUrl + "/removeAdmin", form, "PUT")
|
||||
}
|
||||
|
||||
function deleteUser(userId: number) {
|
||||
let form = new FormData();
|
||||
form.set("user-id", userId.toString());
|
||||
submitFormToUrl(apiUrl + "/deleteUser", form, "DELETE");
|
||||
}
|
||||
|
||||
// ------------------- DECIDE WHERE TO CONNECT -------------
|
||||
let apiUrl: string;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
apiUrl = "http://localhost:8080/api";
|
||||
} else {
|
||||
apiUrl = document.location.protocol + "//" + document.location.host + "/api";
|
||||
}
|
||||
|
||||
// ------------------- OPENID CONNECT STUFF ----------------
|
||||
|
||||
let discoveryUri = localStorage.getItem("sykkelaksjon-discovery-uri");
|
||||
let clientId = localStorage.getItem("sykkelaksjon-client-id");
|
||||
|
||||
if (!discoveryUri || !clientId) {
|
||||
await fetch(apiUrl + "/openid")
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
let received = json as SykkelaksjonOpenid;
|
||||
discoveryUri = received.openid_discovery_uri;
|
||||
clientId = received.client_id;
|
||||
|
||||
localStorage.setItem("sykkelaksjon-discovery-uri", discoveryUri);
|
||||
localStorage.setItem("sykkelaksjon-client-id", clientId);
|
||||
})
|
||||
}
|
||||
|
||||
let refreshToken: string | null = null;
|
||||
let config: openidClient.Configuration | null = null;
|
||||
if (discoveryUri && clientId) {
|
||||
let server: URL = new URL(discoveryUri);
|
||||
config = await openidClient.discovery(server, clientId);
|
||||
|
||||
let getCurrentUrl: ((...args: any) => URL) = () => {
|
||||
return new URL(window.location.href);
|
||||
}
|
||||
|
||||
let currentUrl = getCurrentUrl();
|
||||
let codeVerifier = localStorage.getItem("sykkelaksjon-codeverifier");
|
||||
|
||||
if (!currentUrl.searchParams.has("session_state") || !currentUrl.searchParams.has("iss") || !currentUrl.searchParams.has("code") || !codeVerifier) {
|
||||
let redirectUri = window.location.href;
|
||||
let scope = "openid profile";
|
||||
|
||||
codeVerifier = openidClient.randomPKCECodeVerifier();
|
||||
localStorage.setItem("sykkelaksjon-codeverifier", codeVerifier);
|
||||
let codeChallenge = await openidClient.calculatePKCECodeChallenge(codeVerifier);
|
||||
|
||||
let state: string;
|
||||
|
||||
let parameters: Record<string, string> = {
|
||||
redirect_uri: redirectUri,
|
||||
scope,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
};
|
||||
|
||||
if (!config.serverMetadata().supportsPKCE()) {
|
||||
state = openidClient.randomState();
|
||||
parameters.state = state;
|
||||
localStorage.setItem("sykkelaksjon-state", state);
|
||||
}
|
||||
|
||||
let redirectTo: URL = openidClient.buildAuthorizationUrl(config, parameters);
|
||||
window.location.href = redirectTo.toString();
|
||||
} else {
|
||||
window.history.pushState({}, "", window.location.protocol + "//" + window.location.host);
|
||||
let state = localStorage.getItem("sykkelaksjon-state");
|
||||
|
||||
let authorizationData: Record<string, string> = {
|
||||
pkceCodeVerifier: codeVerifier
|
||||
};
|
||||
if (state) {
|
||||
authorizationData['state'] = state;
|
||||
}
|
||||
|
||||
let tokens = await openidClient.authorizationCodeGrant(
|
||||
config,
|
||||
currentUrl,
|
||||
authorizationData
|
||||
);
|
||||
|
||||
refreshToken = tokens.refresh_token ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- REQUESTING DATA FROM THE API -------------------------------
|
||||
let receivedData = false;
|
||||
const getAccessToken: () => Promise<string | null> = async () => {
|
||||
if (!refreshToken || !config) {
|
||||
return null;
|
||||
}
|
||||
let receivedTokens = await openidClient.refreshTokenGrant(config, refreshToken);
|
||||
refreshToken = receivedTokens.refresh_token ?? null;
|
||||
return receivedTokens.access_token;
|
||||
}
|
||||
|
||||
const submitFormToUrl = (url: string, formData?: FormData, method?: string) => {
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData) {
|
||||
formData = new FormData(activityForm);
|
||||
}
|
||||
|
||||
if (!method) {
|
||||
method = 'POST';
|
||||
}
|
||||
|
||||
getAccessToken().then(token => {
|
||||
if (token) {
|
||||
fetch(
|
||||
url,
|
||||
{
|
||||
method: method,
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
).then(() => refreshData(token));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const refreshData = async (accessToken?: string) => {
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
let possibleAccessToken = await getAccessToken();
|
||||
if (!possibleAccessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessToken = possibleAccessToken;
|
||||
}
|
||||
|
||||
await fetch(apiUrl, {headers: {"Authorization": `Bearer ${accessToken}`}})
|
||||
.then(response => response.json())
|
||||
.then(message => {
|
||||
(Alpine.store('state') as AlpineState).setServerMessage(message)
|
||||
receivedData = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Finally, refresh data
|
||||
await refreshData();
|
||||
// After data is refreshed, display it
|
||||
if (receivedData) {
|
||||
loadingDiv?.setAttribute("style", "display: none");
|
||||
contentDiv?.setAttribute("style", "display: block");
|
||||
}
|
||||
Vendored
+147
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server message for users of the Sykkelaksjon web site
|
||||
*/
|
||||
export interface Sykkelaksjon {
|
||||
/**
|
||||
* The full name of the connected user
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Whether the connected user is an administrator
|
||||
*/
|
||||
isAdmin: boolean;
|
||||
/**
|
||||
* Available types of activities
|
||||
*/
|
||||
activityTypes: ActivityType[];
|
||||
/**
|
||||
* Whether there exists activity types that don't have a conversion factor of 1
|
||||
*/
|
||||
unitConversionNecessary: boolean;
|
||||
/**
|
||||
* Templates that the user might apply to quickly add a new activity
|
||||
*/
|
||||
activityTemplates: ActivityTemplate[];
|
||||
/**
|
||||
* The activities the user has performed
|
||||
*/
|
||||
activities: Activity[];
|
||||
/**
|
||||
* Other users of the web site to create a leaderboard, and more detailed information for administrators
|
||||
*/
|
||||
otherUsers: {
|
||||
/**
|
||||
* The display name of the user
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The earliest date the user has performed an activity on
|
||||
*/
|
||||
earliestActivity?: string;
|
||||
/**
|
||||
* The latest date the user has performed an activity on
|
||||
*/
|
||||
latestActivity?: string;
|
||||
/**
|
||||
* The total amount of kilometers (including converted activities) the user has covered
|
||||
*/
|
||||
totalKilometers?: number;
|
||||
/**
|
||||
* Administrator info: ther user id of the user
|
||||
*/
|
||||
userId?: number;
|
||||
/**
|
||||
* Administrator info: the username of the user
|
||||
*/
|
||||
userName?: string;
|
||||
/**
|
||||
* Administrator info: whether the user listed is still active
|
||||
*/
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* Administrator info: whether the user listed is an administrator
|
||||
*/
|
||||
isAdmin?: boolean;
|
||||
/**
|
||||
* Administrator info: the activity templates this user has defined
|
||||
*/
|
||||
activityTemplates?: ActivityTemplate[];
|
||||
/**
|
||||
* Administrator info: a detailed list of activities the user has performed
|
||||
*/
|
||||
activities?: Activity[];
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* An available activity the user may log
|
||||
*/
|
||||
export interface ActivityType {
|
||||
/**
|
||||
* The database ID of the activity type
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
* The name of the activity type
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The unit this activity type is measured in
|
||||
*/
|
||||
unit: string;
|
||||
/**
|
||||
* The conversion factor from the unit the activity is measured in into kilometers
|
||||
*/
|
||||
conversionFactor: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* Templates that the user has stored in order to quickly add common activities
|
||||
*/
|
||||
export interface ActivityTemplate {
|
||||
/**
|
||||
* The database ID of the activity template
|
||||
*/
|
||||
id?: number;
|
||||
activityType: ActivityType;
|
||||
/**
|
||||
* The name the user has given to this activity
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The length of the activity (in units defined in the activity type)
|
||||
*/
|
||||
numberOfUnits: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* An activity a user has performed at some date
|
||||
*/
|
||||
export interface Activity {
|
||||
/**
|
||||
* The database ID of the activity
|
||||
*/
|
||||
id?: number;
|
||||
activityType: ActivityType;
|
||||
/**
|
||||
* The length of the activity (in units defined in the activity type)
|
||||
*/
|
||||
numberOfUnits: number;
|
||||
/**
|
||||
* The user's description of the activity
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* The date the user performed the activity
|
||||
*/
|
||||
date: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Server message telling where the openId server is
|
||||
*/
|
||||
export interface SykkelaksjonOpenid {
|
||||
/**
|
||||
* The discovery URI of the OpenID provider
|
||||
*/
|
||||
openid_discovery_uri: string;
|
||||
/**
|
||||
* The client ID of the sykkelaksjon client
|
||||
*/
|
||||
client_id: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.outer-vertical-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.vertical-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.register-activity-div {
|
||||
border: 1px gray solid;
|
||||
padding: 10px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.register-activity-div input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.submit-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.standard-activities-div {
|
||||
border: 1px gray solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.registered-activities-div {
|
||||
border: 1px gray solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table,td,th {
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
|
||||
.registered-activities-div td {
|
||||
padding-right:10px;
|
||||
}
|
||||
|
||||
.registered-activities-div td button {
|
||||
margin-left:50px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.other-users-div {
|
||||
border: 1px gray solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.admin-div {
|
||||
border: 1px gray solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
details {
|
||||
border: 1px solid #aaaaaa;
|
||||
border-radius: 4px;
|
||||
padding: 0.5em 0.5em 2px;
|
||||
}
|
||||
|
||||
summary {
|
||||
font-weight: bold;
|
||||
margin: -0.5em -0.5em 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2023",
|
||||
"module": "esnext",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user