Add svelte sample app and expose it on main route

This commit is contained in:
Mikołaj Pich 2023-10-29 12:01:50 +01:00
parent da35c25544
commit fb6ab6a07e
26 changed files with 2488 additions and 3 deletions

3
.gitignore vendored
View file

@ -36,3 +36,6 @@ out/
.vscode/
.env
wulkanowy-*.json
node_modules/
src/main/resources/app

View file

@ -1,3 +1,7 @@
FROM node:21
WORKDIR /home/gradle/src/app
RUN npm install && npm run build
FROM gradle:7-jdk11 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src

10
app/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
app/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

38
app/README.md Normal file
View file

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

17
app/jsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

1674
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
app/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.3"
}
}

12
app/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
app/src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Schools - Wulkanowy</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

1
app/src/lib/index.js Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,2 @@
// This can be false if you're using a fallback (i.e. SPA mode)
export const prerender = true;

View file

@ -0,0 +1,7 @@
<script>
import RemoteTable from "./RemoteTable.svelte";
</script>
<h1>Schools</h1>
<RemoteTable/>

View file

@ -0,0 +1,106 @@
<script context="module">
let globalLabels;
export function setLabels(labels) {
globalLabels = labels;
}
</script>
<script>
import { createEventDispatcher, getContext } from "svelte";
const dispatch = createEventDispatcher();
const stateContext = getContext("state");
export let buttons = [-2, -1, 0, 1, 2];
export let count;
export let page = 0;
export let pageSize;
export let serverSide = false;
export let labels = {
first: "First",
last: "Last",
next: "Next",
previous: "Previous",
...globalLabels
};
$: pageCount = Math.floor(count / pageSize);
function onChange(event, page) {
const state = stateContext.getState();
const detail = {
originalEvent: event,
page,
pageIndex: serverSide ? 0 : page * state.pageSize,
pageSize: state.pageSize
};
dispatch("pageChange", detail);
if (detail.preventDefault !== true) {
stateContext.setPage(detail.page, detail.pageIndex);
}
}
</script>
<style>
.active {
background-color: rgb(150, 150, 235);
color: white;
}
ul {
flex: 1;
float: right;
list-style: none;
}
li {
float: left;
}
button {
background: transparent;
border: 1px solid #ccc;
padding: 5px 10px;
margin-left: 3px;
float: left;
cursor: pointer;
}
</style>
<ul>
<li>
<button disabled={page === 0} on:click={e => onChange(e, 0)}>
{labels.first}
</button>
</li>
<li>
<button disabled={page === 0} on:click={e => onChange(e, page - 1)}>
{labels.previous}
</button>
</li>
{#each buttons as button}
{#if page + button >= 0 && page + button <= pageCount}
<li>
<button
class:active={page === page + button}
on:click={e => onChange(e, page + button)}>
{page + button + 1}
</button>
</li>
{/if}
{/each}
<li>
<button
disabled={page > pageCount - 1}
on:click={e => onChange(e, page + 1)}>
{labels.next}
</button>
</li>
<li>
<button disabled={page >= pageCount} on:click={e => onChange(e, pageCount)}>
{labels.last}
</button>
</li>
</ul>

View file

@ -0,0 +1,84 @@
<script>
//Row component is optional and only serves to render odd/even row, you can use <tr> instead.
//Sort component is optional
import { onMount } from "svelte";
import Table, { Pagination, Row, Search, Sort } from "./Table.svelte";
import { getData } from "./server.js";
import { sortNumber, sortString } from "./sorting.js";
let rows = [];
let page = 0; //first page
let pageIndex = 0; //first row
let pageSize = 3; //optional, 10 by default
let loading = true;
let rowsCount = 0;
let text;
let sorting;
onMount(async () => {
await load(page);
});
async function load(_page) {
loading = true;
const data = await getData(_page, pageSize, text, sorting);
rows = data.rows;
rowsCount = data.rowsCount;
loading = false;
}
function onCellClick(row) {
alert(JSON.stringify(row));
}
function onPageChange(event) {
load(event.detail.page);
page = event.detail.page;
}
async function onSearch(event) {
text = event.detail.text;
await load(page);
page = 0;
}
async function onSort(event) {
sorting = { dir: event.detail.dir, key: event.detail.key };
await load(page);
}
</script>
<Table {loading} {rows} {pageIndex} {pageSize} let:rows={rows2}>
<div slot="top">
<Search on:search={onSearch} />
</div>
<thead slot="head">
<tr>
<th>
Name
<Sort key="name" on:sort={onSort} />
</th>
<th>
Lastname
<Sort key="lastName" on:sort={onSort} />
</th>
<th>
Age
<Sort key="age" on:sort={onSort} />
</th>
</tr>
</thead>
<tbody>
{#each rows2 as row, index (row)}
<Row {index} on:click={() => onCellClick(row)}>
<td data-label="Name">{row.name}</td>
<td data-label="Lastname">{row.lastName}</td>
<td data-label="Age">{row.age}</td>
</Row>
{/each}
</tbody>
<div slot="bottom">
<Pagination {page} {pageSize} count={rowsCount} serverSide={true} on:pageChange={onPageChange} />
</div>
</Table>

24
app/src/routes/Row.svelte Normal file
View file

@ -0,0 +1,24 @@
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let index = 0;
function onClick(event) {
dispatch("click", event);
}
</script>
<style>
.odd {
background-color: #f7f7f7;
}
</style>
<tr
on:click={onClick}
class={$$props.class}
class:odd={index % 2 !== 0}
class:even={index % 2 === 0}>
<slot />
</tr>

View file

@ -0,0 +1,91 @@
<script context="module">
let globalLabels;
export function setLabels(labels) {
globalLabels = labels;
}
</script>
<script>
import { createEventDispatcher, getContext } from "svelte";
const dispatch = createEventDispatcher();
const stateContext = getContext("state");
export let filter = (row, text, index) => {
text = text.toLowerCase();
for (let i in row) {
if (
row[i]
.toString()
.toLowerCase()
.indexOf(text) > -1
) {
return true;
}
}
return false;
};
export let index = -1;
export let text = "";
export let labels = {
placeholder: "Search",
...globalLabels
};
async function onSearch(event) {
const state = stateContext.getState();
const detail = {
originalEvent: event,
filter,
index,
text,
page: state.page,
pageIndex: state.pageIndex,
pageSize: state.pageSize,
rows: state.filteredRows
};
dispatch("search", detail);
if (detail.preventDefault !== true) {
if (detail.text.length === 0) {
stateContext.setRows(state.rows);
} else {
stateContext.setRows(
detail.rows.filter(r => detail.filter(r, detail.text, index))
);
}
stateContext.setPage(0, 0);
} else {
stateContext.setRows(detail.rows);
}
}
</script>
<style>
.search {
width: 33.3%;
float: right;
}
.search input {
width: 100%;
border: 1px solid #eee;
border-radius: 3px;
padding: 5px 3px;
}
@media screen and (max-width: 767px) {
.search {
width: 100%;
}
}
</style>
<div class="search">
<input
type="search"
title={labels.placeholder}
placeholder={labels.placeholder}
bind:value={text}
on:keyup={onSearch} />
</div>

View file

@ -0,0 +1,67 @@
<script context="module">
let globalLabels;
export function setLabels(labels) {
globalLabels = labels;
}
</script>
<script>
import { createEventDispatcher, getContext } from "svelte";
const dispatch = createEventDispatcher();
const stateContext = getContext("state");
export let dir = "none";
export let key;
export let labels = {
asc: { title: "Ascending", html: "&#8593;" },
desc: { title: "Desceding", html: "&#8595;" },
unsorted: { title: "Unsorted", html: "&#8645;" },
...globalLabels
};
function onClick(event) {
const state = stateContext.getState();
let rows;
const detail = {
originalEvent: event,
key,
dir: dir !== "desc" ? "desc" : "asc",
rows: state.filteredRows
};
dispatch("sort", detail);
if (detail.preventDefault !== true) {
dir = detail.dir;
}
stateContext.setRows(detail.rows);
}
</script>
<style>
.sort {
right: 0;
cursor: pointer;
position: absolute;
padding: 0 0.25em;
color: #999;
}
</style>
<span class="sort" on:click={onClick}>
{#if dir === 'asc'}
<span title={labels.asc.title}>
{@html labels.asc.html}
</span>
{:else if dir === 'desc'}
<span title={labels.desc.title}>
{@html labels.desc.html}
</span>
{:else}
<span title={labels.unsorted.title}>
{@html labels.unsorted.html}
</span>
{/if}
</span>

200
app/src/routes/Table.svelte Normal file
View file

@ -0,0 +1,200 @@
<script context="module">
import Pagination, {
setLabels as _setPaginationLabels
} from "./Pagination.svelte";
import Row from "./Row.svelte";
import Search, { setLabels as _setSearchLabels } from "./Search.svelte";
import Sort, { setLabels as _setSortLabels } from "./Sort.svelte";
export { Pagination, Row, Search, Sort };
let globalLabels;
export function setTableLabels(labels) {
globalLabels = labels;
}
export const setPaginationLabels = _setPaginationLabels;
export const setSearchLabels = _setSearchLabels;
export const setSortLabels = _setSortLabels;
</script>
<script>
import { createEventDispatcher, setContext } from "svelte";
const dispatch = createEventDispatcher();
export let loading = false;
export let page = 0;
export let pageIndex = 0;
export let pageSize = 10;
export let responsive = true;
export let rows;
export let serverSide = false;
export let labels = {
empty: "No records available",
loading: "Loading data",
...globalLabels
};
let buttons = [-2, -1, 0, 1, 2];
let pageCount = 0;
$: filteredRows = rows;
$: visibleRows = filteredRows.slice(pageIndex, pageIndex + pageSize);
setContext("state", {
getState: () => ({
page,
pageIndex,
pageSize,
rows,
filteredRows
}),
setPage: (_page, _pageIndex) => {
page = _page;
pageIndex = _pageIndex;
},
setRows: _rows => (filteredRows = _rows)
});
function onPageChange(event) {
dispatch("pageChange", event.detail);
}
function onSearch(event) {
dispatch("search", event.detail);
}
</script>
<style>
.table {
width: 100%;
border-collapse: collapse;
}
.table :global(td) {
position: relative;
}
.table :global(th) {
position: relative;
}
.table :global(td) {
padding: 0.3em 0.3em;
}
.center {
text-align: center;
font-style: italic;
}
.center > span {
padding: 10px 10px;
float: left;
width: 100%;
}
.slot-top,
.slot-bottom {
float: left;
width: 100%;
margin-top: 1em;
}
@media screen and (max-width: 767px) {
table.responsive {
border: 0;
}
table.responsive :global(thead) {
border: none;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
table.responsive :global(tr) {
border-bottom: 2px solid #ddd;
display: block;
padding-bottom: 0.3em;
margin-bottom: 0.3em;
}
table.responsive :global(td) {
border-bottom: 1px solid #ddd;
display: block;
font-size: 0.8em;
text-align: right;
}
table.responsive :global(td::before) {
/*
* aria-label has no advantage, it won't be read inside a table content: attr(aria-label);
*/
content: attr(data-label);
float: left;
font-weight: bold;
}
table.responsive :global(td[data-label-normal]::before) {
font-weight: normal;
}
table.responsive :global(td[data-label-upper]::before) {
text-transform: uppercase;
}
table.responsive :global(td:last-child) {
border-bottom: 0;
}
}
</style>
<slot name="top">
<div class="slot-top">
<svelte:component this={Search} on:search={onSearch} />
</div>
</slot>
<table class={'table ' + $$props.class} class:responsive>
<slot name="head" />
{#if loading}
<tbody>
<tr>
<td class="center" colspan="100%">
<span>
{@html labels.loading}
</span>
</td>
</tr>
</tbody>
{:else if visibleRows.length === 0}
<tbody>
<tr>
<td class="center" colspan="100%">
<span>
{@html labels.empty}
</span>
</td>
</tr>
</tbody>
{:else}
<slot rows={visibleRows} />
{/if}
<slot name="foot" />
</table>
<slot name="bottom">
<div class="slot-bottom">
<svelte:component
this={Pagination}
{page}
{pageSize}
{serverSide}
count={filteredRows.length - 1}
on:pageChange={onPageChange} />
</div>
</slot>

64
app/src/routes/server.js Normal file
View file

@ -0,0 +1,64 @@
import { sortNumber, sortString } from "./sorting.js";
function generateData() {
const rand = Math.floor(Math.random() * 1000);
return [
{ name: "a-" + rand.toString(), lastName: "o", age: 12 },
{ name: "b", lastName: "n", age: 1 },
{ name: "c", lastName: "m", age: 13 },
{ name: "d", lastName: "l", age: 21 },
{ name: "e", lastName: "k", age: 2 },
{ name: "f", lastName: "j", age: 4 }
];
}
export function getAll(text) {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve(generateData());
}, 500);
});
}
export function getData(page, pageSize, text, sorting) {
let originalData = generateData();
if (sorting) {
if (sorting.key === "age") {
originalData = sortNumber(originalData, sorting.dir, sorting.key);
} else {
originalData = sortString(originalData, sorting.dir, sorting.key);
}
}
return new Promise((resolve, reject) => {
setTimeout(function() {
let rowsCount = originalData.length;
const originalRows = originalData;
let rows = [];
if (text && text.length > 0) {
for (let i in originalRows) {
for (let j in originalRows[i]) {
if (
originalRows[i][j]
.toString()
.toLowerCase()
.indexOf(text) > -1
) {
rows.push(originalRows[i]);
break;
}
}
}
rowsCount = rows.length;
} else {
rows = originalRows;
}
resolve({ rows: rows.slice(0, pageSize), rowsCount: rowsCount - 1 });
}, 500);
});
}

13
app/src/routes/sorting.js Normal file
View file

@ -0,0 +1,13 @@
export function sortString(rows, dir, key) {
return rows.sort((a, b) =>
dir === "asc"
? ("" + a[key]).localeCompare(b[key])
: ("" + b[key]).localeCompare(a[key])
);
}
export function sortNumber(rows, dir, key) {
return rows.sort((a, b) =>
dir === "asc" ? a[key] - b[key] : b[key] - a[key]
);
}

0
app/static/favicon.ico Normal file
View file

15
app/svelte.config.js Normal file
View file

@ -0,0 +1,15 @@
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
// default options are shown. On some platforms
// these options are set automatically — see below
pages: '../src/main/resources/app',
assets: '../src/main/resources/app',
fallback: undefined,
precompress: false,
strict: true
})
}
};

6
app/vite.config.js Normal file
View file

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

View file

@ -8,8 +8,12 @@ import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
embeddedServer(Netty, port = 3002, host = "0.0.0.0", module = Application::module)
.start(wait = true)
embeddedServer(
factory = Netty,
port = 3002,
host = "0.0.0.0",
module = Application::module
).start(wait = true)
}
fun Application.module() {

View file

@ -5,6 +5,7 @@ import io.github.wulkanowy.schools.integrity.*
import io.github.wulkanowy.schools.model.LoginEvent
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -36,8 +37,13 @@ fun Application.configureRouting() {
}
}
}
get("/") {
get("/log/list") {
call.respond(loginEventDao.allLoginEvents())
}
singlePageApplication {
useResources = true
filesPath = "app"
defaultPage = "index.html"
}
}
}