Merge branch '43-store-entry-authorship' into '22-integrate-api-and-frontend'

Resolve "Store entry authorship"

See merge request padas/24ss-5430-web-and-data-eng/gruppe-3/datadash!42
This commit is contained in:
Elias Schriefer 2024-07-06 14:46:41 +02:00
commit 3f857f023e
11 changed files with 491 additions and 41 deletions

View File

@ -1,9 +1,5 @@
@import url("main.css");
:root {
--accent-color: oklch(65.33% 0.158 247.76);
}
form {
display: grid;
grid-template-columns: 1fr 1fr auto;

View File

@ -1,3 +1,5 @@
import Dataset from "./dataset.js";
const form = document.forms[0];
const {
title: titleEntry,
@ -23,7 +25,11 @@ const validationListener = () => {
fullDescriptionEntry,
].forEach(input => input.addEventListener("input", validationListener));
form.addEventListener("submit", e => {
cancelBtn.addEventListener("click", () => {
window.location.href = location.origin;
})
form.addEventListener("submit", async e => {
e.preventDefault();
if (!form.reportValidity()) return;
@ -43,17 +49,20 @@ form.addEventListener("submit", e => {
// Don't allow several requests to be sent at the same time
addBtn.disabled = true;
fetch("/api/v1/datasets", {
let response = await fetch("/api/v1/datasets", {
method: "POST",
body: JSON.stringify(newContent),
headers: {
"Content-Type": "application/json;charset=utf-8"
}
}).then(response => {
if (response.status == 200) {
location.assign("/");
} else {
addBtn.disabled = !form.checkValidity();
}
});
let data = await response.json();
let dataset = new Dataset(data);
dataset.storageSetKey("created-locally", true);
if (response.ok) {
location.assign("/");
} else {
addBtn.disabled = !form.checkValidity();
}
});

View File

@ -0,0 +1,2 @@
export const DATASET_ENDPOINT = "/api/v1/datasets";
export const getBaseURL = () => location.origin;

View File

@ -1,13 +1,13 @@
import { DATASET_ENDPOINT, getBaseURL } from "./main.js";
import { DATASET_ENDPOINT, getBaseURL } from "./constants.js";
export default class Dataset {
static #datasets = new Map();
#abstract;
#shortDescription;
#author;
#categories;
#category;
#date;
#description;
#fullDescription;
#id;
#rating;
#title;
@ -15,48 +15,52 @@ export default class Dataset {
#upvotes;
#url;
#votes;
#license;
#termsOfUse;
#elements = [];
static get(id) {
return this.#datasets.get(id);
}
constructor({ abst: shortDescription, author, categories, date, description, id, rating, title, type, upvotes, url, votes }) {
this.#abstract = shortDescription;
constructor({ abst: shortDescription, author, categorie, date, description, id, raiting, title, type, upvotes, url, votes, license, termsOfUse }) {
this.#shortDescription = shortDescription;
this.#author = author;
this.#categories = categories;
this.#category = categorie;
this.#date = date;
this.#description = description;
this.#fullDescription = description;
this.#id = id;
this.#rating = rating;
this.#rating = raiting;
this.#title = title;
this.#type = type;
this.#upvotes = upvotes;
this.#url = url;
this.#votes = votes;
this.#license = license;
this.#termsOfUse = termsOfUse;
Dataset.#datasets.set(id, this);
}
// Getters
get abstract() {
return this.#abstract;
get shortDescription() {
return this.#shortDescription;
}
get author() {
return this.#author;
}
get categories() {
return this.#categories;
get category() {
return this.#category;
}
get date() {
return this.#date;
}
get description() {
return this.#description;
get fullDescription() {
return this.#fullDescription;
}
get id() {
@ -87,6 +91,14 @@ export default class Dataset {
return this.#votes;
}
get license() {
return this.#license;
}
get termsOfUse() {
return this.#termsOfUse;
}
get mainElement() {
return this.#elements[0];
}
@ -95,6 +107,10 @@ export default class Dataset {
return this.#elements;
}
parseDate() {
return new Date(this.#date);
}
// Main methods
// Only on main page
@ -114,7 +130,7 @@ export default class Dataset {
}
})
clone.querySelector(".dataset-title").innerText = this.#title;
clone.querySelector(".dataset-description").innerText = this.#description;
clone.querySelector(".dataset-description").innerText = this.#shortDescription;
clone.querySelector(".upvote-count").innerText = this.#upvotes;
this.#elements.push(clone.children[0]);

View File

@ -0,0 +1,180 @@
@import url("./main.css");
@import url('https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap');
:root {
--min-card-size: min(60ch, calc(100vw - var(--pad-main)));
--rating-color: gold;
}
main {
& > :is(header, section, footer) {
background-color: var(--fg-color);
padding: var(--pad-datasets-block) var(--pad-datasets-inline);
position: relative;
}
& > :first-child {
border-radius: var(--corner-radius) var(--corner-radius) 0 0;
margin-top: var(--pad-main);
}
& > :last-child {
border-radius: 0 0 var(--corner-radius) var(--corner-radius);
margin-bottom: var(--pad-main);
}
& > :not(:last-child):is(header, section)::after {
content: '';
position: absolute;
inset-inline: calc(var(--pad-datasets-inline) - var(--gap-small));
bottom: 0;
border-bottom: 3px solid var(--bg-color);
opacity: .25;
transform: translateY(50%);
z-index: 1;
}
}
header {
margin-inline: 0;
display: grid;
grid-template-columns: 1fr 1fr max-content;
align-items: center;
grid-gap: var(--gap-medium);
}
#title {
grid-column: 1 / 3;
margin-block: var(--gap-medium) 0;
text-align: center;
&::after {
content: attr(data-type);
background-color: var(--accent-color);
font-size: .5em;
font-weight: initial;
position: relative;
bottom: .25lh;
margin-inline: var(--gap-small);
padding: 2pt 4pt;
border-radius: 4pt;
}
&[data-type="dataset"]::after {
content: "Dataset";
}
&[data-type="api"]::after {
content: "API";
}
}
:has(#rating), #url {
text-align: start;
grid-column: 1 / 3;
}
#rating {
color: color-mix(in oklab, var(--text-color) 80%, black);
}
#rating::after {
content: "";
display: inline-block;
width: 5em;
height: 1lh;
vertical-align: bottom;
margin-inline: .5ch;
mask-image: url("stars.svg");
-webkit-mask-image: url("stars.svg");
mask-size: 100% 100%;
mask-mode: alpha;
--rating-percent: calc((var(--rating, 0) / 5) * 100%);
background: linear-gradient(to right, var(--rating-color) var(--rating-percent), var(--bg-color) var(--rating-percent));
}
a {
--text-color: var(--accent-color);
/*
Why doesn't it do this automatically? It is inherited from body,
so I should be able to just change --text-color, right?
*/
color: var(--text-color);
}
#terms-of-use {
/* text-align: end; */
}
.upvote {
margin: var(--gap-medium) 0;
align-self: self-start;
grid-column: 3;
grid-row: 1 / 4;
}
#metadata {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: var(--gap-large);
}
#full-description {
text-wrap: balance;
text-wrap: pretty;
margin-top: 0;
br {
margin-bottom: .5lh;
}
}
.skeleton {
font-family: "Flow Circular";
font-weight: 400;
font-style: normal;
user-select: none;
--rating-color: var(--text-color);
a, .btn {
pointer-events: none;
}
& > * {
cursor: progress;
}
@media screen and not (prefers-reduced-motion) {
:is(header, section) > :is(p, span, h1, a) {
animation: infinite 2s linear skeleton-loading;
background: radial-gradient(
circle farthest-side,
var(--highlight-color, white) 20%,
var(--text-color) 40%
) no-repeat, var(--text-color);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
&:is(a) {
--highlight-color: color-mix(in oklab, white, var(--text-color));
}
}
}
#title::after {
color: color-mix(in oklab, var(--accent-color) 50%, currentcolor);
}
}
@keyframes skeleton-loading {
from {
background-position-x: calc(-1.4 * var(--min-card-size)), 0;
}
to {
background-position-x: calc(1.4 * var(--min-card-size)), 0;
}
}

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dataset details</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap" crossorigin>
<link rel="stylesheet" href="details.css">
<script defer type="module" src="details.js"></script>
</head>
<body>
<template id="voting-template">
<aside class="upvote">
<button class="upvote-btn btn flat">Upvote</button>
<span class="upvote-count">0</span>
<button class="downvote-btn btn flat">Downvote</button>
</aside>
</template>
<main class="skeleton">
<header data-id="dataset-id">
<h1 id="title" data-type="api">Title</h1>
<span>
<span id="rating" style="--rating: 4">4</span>
<span id="short-description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis recusandae laborum odio corrupti voluptas quisquam dicta, quibusdam ipsum qui exercitationem.</span>
</span>
<a id="url">https://example.com/dataset</a>
<aside class="upvote">
<button disabled class="upvote-btn btn flat">Upvote</button>
<span class="upvote-count">0</span>
<button disabled class="downvote-btn btn flat">Downvote</button>
</aside>
</header>
<section id="metadata">
<span>Added on: <span id="date" data-date="0">1. January 1970</span></span>
<span>Category: <span id="category">Something</span></span>
<span>License: <span id="license">MIT</span></span>
<a id="terms-of-use">Terms of Use</a>
</section>
<section id="details">
<p id="full-description">Full description<br>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Beatae
nihil saepe et numquam quo id voluptatum recusandae assumenda
doloremque pariatur consequatur molestias delectus dolore
corrupti, odio modi vitae repellat tempora sed eos voluptates
temporibus veritatis repudiandae. Cum eveniet molestias, in
beatae non reiciendis quia voluptatem id, facere architecto
vitae harum ipsum earum deleniti dolor atque recusandae odit
corporis error dolorum blanditiis vel maxime pariatur quibusdam!
<br>Saepe debitis ab possimus, dolorem neque ad voluptatibus ex
quisquam itaque. Nihil et non consequuntur error ipsa.
Necessitatibus voluptatibus aliquid itaque id ipsum, pariatur
odio explicabo, dolores ex, incidunt tenetur dolore. Assumenda
ipsam nobis quis.
</p>
</section>
</main>
</body>
</html>

View File

@ -0,0 +1,51 @@
import Dataset from "./dataset.js";
const mainElement = document.getElementsByTagName("main")[0];
const title = document.getElementById("title");
const rating = document.getElementById("rating");
const shortDescription = document.getElementById("short-description");
const url = document.getElementById("url");
const date = document.getElementById("date");
const category = document.getElementById("category");
const license = document.getElementById("license");
const termsOfUse = document.getElementById("terms-of-use");
const fullDescription = document.getElementById("full-description");
const currentLocation = new URL(location.href);
if (currentLocation.searchParams.has("id")) {
const id = currentLocation.searchParams.get("id");
const response = await fetch(`${currentLocation.origin}/api/v1/datasets/id/${id}`);
console.dir(response);
if (response.ok) {
const data = await response.json();
const dataset = new Dataset(data);
console.dir(data, dataset);
const upvoteComponent = dataset.createUpvoteComponent();
title.innerText = dataset.title;
title.dataset.type = dataset.type.toLowerCase();
rating.innerText = dataset.rating;
rating.style.setProperty("--rating", `${dataset.rating}`);
shortDescription.innerText = dataset.shortDescription;
url.href = dataset.url;
url.innerText = dataset.url;
mainElement.querySelector(".upvote").replaceWith(upvoteComponent);
date.innerText = dataset.parseDate().toLocaleDateString(undefined, {
day: "numeric",
month: "long",
year: "numeric",
});
date.dataset.date = dataset.parseDate().getTime();
category.innerText = dataset.category.name;
category.dataset.id = dataset.category.id;
license.innerText = dataset.license;
termsOfUse.href = dataset.termsOfUse;
fullDescription.innerText = dataset.fullDescription;
mainElement.classList.remove("skeleton");
}
}

View File

@ -4,6 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DataDash</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="prefetch" href="https://fonts.googleapis.com/css2?family=Flow+Circular&display=swap">
<link rel="stylesheet" href="main.css">
<script type="module" src="main.js" defer></script>
</head>

View File

@ -2,7 +2,12 @@
--bg-color: #222;
--fg-color: #555;
--text-color: #dbdbdb;
--pad-datasets: 1rem;
--accent-color: oklch(65.33% 0.158 247.76);
--pad-datasets-block: 1rem;
--pad-datasets-inline: 2rem;
--gap-large: 1.5rem;
--gap-medium: 1rem;
--gap-small: .5rem;
--pad-main: 2rem;
--min-card-size: min(60ch, 33vw);
--corner-radius: 1rem;
@ -17,7 +22,7 @@ body {
}
main {
max-width: calc(2 * var(--min-card-size) + var(--pad-main) + var(--pad-datasets));
max-width: calc(2 * var(--min-card-size) + var(--pad-main) + 2 * var(--pad-datasets-inline));
padding-inline: var(--pad-main);
margin-inline: auto;
container-type: inline-size;
@ -47,12 +52,12 @@ header {
display: flex;
flex-direction: row;
float: right;
gap: .5rem;
gap: var(--gap-small);
background-color: var(--fg-color, darkgrey);
padding: .5rem 1rem;
margin-bottom: var(--pad-datasets);
margin-left: var(--pad-datasets);
margin-right: var(--pad-datasets);
margin-bottom: var(--pad-datasets-block);
margin-left: var(--pad-datasets-inline);
margin-right: var(--pad-datasets-inline);
border-radius: 1.5rem;
}
@ -83,10 +88,10 @@ header {
}
.datasets {
padding-inline: var(--pad-datasets);
padding-inline: var(--pad-datasets-inline);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(var(--min-card-size), 1fr));
gap: 1rem;
gap: var(--gap-medium);
}
.hidden {
@ -101,7 +106,7 @@ header {
}
.dataset {
padding: var(--pad-datasets) 2rem;
padding: var(--pad-datasets-block) var(--pad-datasets-inline);
background-color: var(--fg-color, darkgrey);
border-radius: var(--corner-radius);
list-style: none;
@ -122,7 +127,7 @@ header {
display: flex;
flex-direction: column;
align-items: center;
gap: .5em;
gap: var(--gap-small);
}
/* Buttons */

View File

@ -1,9 +1,7 @@
import { DATASET_ENDPOINT, getBaseURL } from "./constants.js";
import { fetchQuery } from "./contentUtility.js";
import Dataset from "./dataset.js";
export const DATASET_ENDPOINT = "/api/v1/datasets";
export const getBaseURL = () => location.origin;
const defaultPagingValue = 20;
export const lastQuery = {
totalPages: 0,

View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg"
width="4.52333in" height="0.853333in"
viewBox="0 0 1357 256">
<path id="Selection"
fill="black" stroke="black" stroke-width="1"
d="M 145.00,19.00
C 148.26,23.95 151.83,34.12 154.20,40.00
154.20,40.00 167.00,71.00 167.00,71.00
168.58,74.95 174.23,90.23 176.65,92.40
178.81,94.36 189.75,94.96 193.00,95.00
193.00,95.00 256.00,98.00 256.00,98.00
252.97,103.56 245.88,108.30 241.00,112.42
241.00,112.42 212.00,137.08 212.00,137.08
209.10,139.50 197.48,148.71 196.46,151.43
195.50,153.97 199.97,168.40 200.87,172.00
200.87,172.00 216.00,229.00 216.00,229.00
216.00,229.00 168.00,199.95 168.00,199.95
164.08,197.50 150.78,188.29 147.00,188.29
143.53,188.30 130.58,197.29 127.00,199.67
127.00,199.67 79.00,230.00 79.00,230.00
79.00,230.00 92.79,173.00 92.79,173.00
92.79,173.00 96.68,152.17 96.68,152.17
96.68,152.17 80.00,137.39 80.00,137.39
80.00,137.39 52.00,114.42 52.00,114.42
52.00,114.42 36.00,100.00 36.00,100.00
36.00,100.00 92.00,96.09 92.00,96.09
96.36,95.79 111.98,95.53 114.78,93.40
118.14,90.85 122.99,76.57 124.68,72.00
124.68,72.00 145.00,19.00 145.00,19.00 Z
M 411.00,19.00
C 414.26,23.95 417.83,34.12 420.20,40.00
420.20,40.00 433.00,71.00 433.00,71.00
434.58,74.95 440.23,90.23 442.65,92.40
444.81,94.36 455.75,94.96 459.00,95.00
459.00,95.00 522.00,98.00 522.00,98.00
518.97,103.56 511.88,108.30 507.00,112.42
507.00,112.42 478.00,137.08 478.00,137.08
475.10,139.50 463.48,148.71 462.46,151.43
461.50,153.97 465.97,168.40 466.87,172.00
466.87,172.00 482.00,229.00 482.00,229.00
482.00,229.00 434.00,199.95 434.00,199.95
430.08,197.50 416.78,188.29 413.00,188.29
409.53,188.30 396.58,197.29 393.00,199.67
393.00,199.67 345.00,230.00 345.00,230.00
345.00,230.00 358.79,173.00 358.79,173.00
358.79,173.00 362.68,152.17 362.68,152.17
362.68,152.17 346.00,137.39 346.00,137.39
346.00,137.39 318.00,114.42 318.00,114.42
318.00,114.42 302.00,100.00 302.00,100.00
302.00,100.00 358.00,96.09 358.00,96.09
362.36,95.79 377.98,95.53 380.78,93.40
384.14,90.85 388.99,76.57 390.68,72.00
390.68,72.00 411.00,19.00 411.00,19.00 Z
M 678.00,19.00
C 681.26,23.95 684.83,34.12 687.20,40.00
687.20,40.00 700.00,71.00 700.00,71.00
701.58,74.95 707.23,90.23 709.65,92.40
711.81,94.36 722.75,94.96 726.00,95.00
726.00,95.00 789.00,98.00 789.00,98.00
785.97,103.56 778.88,108.30 774.00,112.42
774.00,112.42 745.00,137.08 745.00,137.08
742.10,139.50 730.48,148.71 729.46,151.43
728.50,153.97 732.97,168.40 733.87,172.00
733.87,172.00 749.00,229.00 749.00,229.00
749.00,229.00 701.00,199.95 701.00,199.95
697.08,197.50 683.78,188.29 680.00,188.29
676.53,188.30 663.58,197.29 660.00,199.67
660.00,199.67 612.00,230.00 612.00,230.00
612.00,230.00 625.79,173.00 625.79,173.00
625.79,173.00 629.68,152.17 629.68,152.17
629.68,152.17 613.00,137.39 613.00,137.39
613.00,137.39 585.00,114.42 585.00,114.42
585.00,114.42 569.00,100.00 569.00,100.00
569.00,100.00 625.00,96.09 625.00,96.09
629.36,95.79 644.98,95.53 647.78,93.40
651.14,90.85 655.99,76.57 657.68,72.00
657.68,72.00 678.00,19.00 678.00,19.00 Z
M 944.00,19.00
C 947.26,23.95 950.83,34.12 953.20,40.00
953.20,40.00 966.00,71.00 966.00,71.00
967.58,74.95 973.23,90.23 975.65,92.40
977.81,94.36 988.75,94.96 992.00,95.00
992.00,95.00 1055.00,98.00 1055.00,98.00
1051.97,103.56 1044.88,108.30 1040.00,112.42
1040.00,112.42 1011.00,137.08 1011.00,137.08
1008.10,139.50 996.48,148.71 995.46,151.43
994.50,153.97 998.97,168.40 999.87,172.00
999.87,172.00 1015.00,229.00 1015.00,229.00
1015.00,229.00 967.00,199.95 967.00,199.95
963.08,197.50 949.78,188.29 946.00,188.29
942.53,188.30 929.58,197.29 926.00,199.67
926.00,199.67 878.00,230.00 878.00,230.00
878.00,230.00 891.79,173.00 891.79,173.00
891.79,173.00 895.68,152.17 895.68,152.17
895.68,152.17 879.00,137.39 879.00,137.39
879.00,137.39 851.00,114.42 851.00,114.42
851.00,114.42 835.00,100.00 835.00,100.00
835.00,100.00 891.00,96.09 891.00,96.09
895.36,95.79 910.98,95.53 913.78,93.40
917.14,90.85 921.99,76.57 923.68,72.00
923.68,72.00 944.00,19.00 944.00,19.00 Z
M 1211.00,19.00
C 1214.26,23.95 1217.83,34.12 1220.20,40.00
1220.20,40.00 1233.00,71.00 1233.00,71.00
1234.58,74.95 1240.23,90.23 1242.65,92.40
1244.81,94.36 1255.75,94.96 1259.00,95.00
1259.00,95.00 1322.00,98.00 1322.00,98.00
1318.97,103.56 1311.88,108.30 1307.00,112.42
1307.00,112.42 1278.00,137.08 1278.00,137.08
1275.10,139.50 1263.48,148.71 1262.46,151.43
1261.50,153.97 1265.97,168.40 1266.87,172.00
1266.87,172.00 1282.00,229.00 1282.00,229.00
1282.00,229.00 1234.00,199.95 1234.00,199.95
1230.08,197.50 1216.78,188.29 1213.00,188.29
1209.53,188.30 1196.58,197.29 1193.00,199.67
1193.00,199.67 1145.00,230.00 1145.00,230.00
1145.00,230.00 1158.79,173.00 1158.79,173.00
1158.79,173.00 1162.68,152.17 1162.68,152.17
1162.68,152.17 1146.00,137.39 1146.00,137.39
1146.00,137.39 1118.00,114.42 1118.00,114.42
1118.00,114.42 1102.00,100.00 1102.00,100.00
1102.00,100.00 1158.00,96.09 1158.00,96.09
1162.36,95.79 1177.98,95.53 1180.78,93.40
1184.14,90.85 1188.99,76.57 1190.68,72.00
1190.68,72.00 1211.00,19.00 1211.00,19.00 Z" />
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB