Add gradle-managed frontend

It still uses NPM (via gradle tasks) but drops the old Angular-based UI in favor of a Lit-based UI from https://github.com/pwa-builder/pwa-starter. I'll have to rebuild from scratch, but I think it'll be better in the long run

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2022-12-15 21:39:22 -06:00
parent 1d15b116c2
commit 73bfc793c8
45 changed files with 8056 additions and 62 deletions

View file

@ -1,5 +1,4 @@
import java.net.URI
import java.util.*
plugins {
java
@ -20,7 +19,7 @@ dependencies {
implementation(project(":api"))
implementation(project(":core"))
implementation(project(":db"))
implementation(project(":web"))
implementation(project(":frontend"))
implementation(libs.kotlin.reflect)
implementation(libs.bundles.ktor.server)
implementation(libs.kotlinx.coroutines.core)

View file

@ -5,7 +5,6 @@ import com.wbrawner.twigs.*
import com.wbrawner.twigs.db.*
import com.wbrawner.twigs.model.Session
import com.wbrawner.twigs.storage.*
import com.wbrawner.twigs.web.webRoutes
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.http.*

View file

@ -1,4 +1,4 @@
package com.wbrawner.twigs.web
package com.wbrawner.twigs.server
import io.ktor.server.application.*
import io.ktor.server.http.content.*
@ -9,12 +9,13 @@ import io.ktor.server.routing.*
fun Application.webRoutes() {
routing {
static {
resources("twigs")
default("index.html")
staticBasePackage = "static"
defaultResource("index.html")
resources(".")
}
intercept(ApplicationCallPipeline.Setup) {
if (!call.request.path().startsWith("/api") && !call.request.path().matches(Regex(".*\\.\\w+$"))) {
call.resolveResource("twigs/index.html")?.let {
call.resolveResource("static/index.html")?.let {
call.respond(it)
return@intercept finish()
}

8
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
node_modules
dist
dev-dist
build
types
.idea
.github

23
frontend/build.gradle.kts Normal file
View file

@ -0,0 +1,23 @@
plugins {
`java-library`
alias(libs.plugins.node.gradle)
}
tasks.register<com.github.gradle.node.npm.task.NpmTask>("package") {
group = "build"
inputs.files(fileTree("node_modules"))
inputs.files(fileTree("src"))
inputs.file("package.json")
inputs.file("swa-cli.config.json")
inputs.file("tsconfig.json")
inputs.file("vite.config.ts")
outputs.dir("build/resources/main/static")
dependsOn.add(tasks.getByName("npmInstall"))
args.set(listOf("run", "build"))
}
tasks.getByName("processResources") {
dependsOn.add("package")
}

52
frontend/index.html Normal file
View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>PWA Starter</title>
<base href="/"/>
<!-- This meta viewport ensures the webpage's dimensions change according to the device it's on. This is called Responsive Web Design.-->
<meta name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0"/>
<meta name="description" content="This is a PWA Starter app"/>
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#181818"/>
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f3f3f3"/>
<!-- These meta tags are Apple-specific, and set the web application to run in full-screen mode with a black status bar. Learn more at https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html-->
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-title" content="PWA Starter"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<!-- Imports an icon to represent the document. -->
<link rel="icon" href="/assets/icons/icon_24.png" type="image/x-icon"/>
<!-- Imports the manifest to represent the web application. A web app must have a manifest to be a PWA. -->
<link rel="manifest" href="/manifest.json"/>
<!-- light mode and dark mode CSS -->
<link rel="stylesheet" media="(prefers-color-scheme:light)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/themes/light.css">
<link rel="stylesheet" media="(prefers-color-scheme:dark)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.73/dist/themes/dark.css"
onload="document.documentElement.classList.add('sl-theme-dark');">
<script type="module" src="/src/app-index.ts"></script>
</head>
<body>
<!-- Our app-index web component. This component is defined in src/pages/app-index.ts-->
<app-index></app-index>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(
'/sw.js'
);
}
</script>
</body>
</html>

7350
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

41
frontend/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "pwa-starter",
"version": "0.0.1",
"description": "A starter kit for building PWAs!",
"main": "index.js",
"scripts": {
"dev-server": "vite --open",
"dev": "npm run dev-server",
"dev-task": "vite",
"deploy": " npx @azure/static-web-apps-cli login --no-use-keychain && npx @azure/static-web-apps-cli deploy",
"build": "tsc && vite build",
"start": "npm run dev",
"start-remote": "vite --host"
},
"author": "",
"license": "ISC",
"dependencies": {
"@pwabuilder/pwainstall": "^1.6.7",
"@shoelace-style/shoelace": "^2.0.0-beta.82",
"@vaadin/router": "^1.7.4",
"lit": "^2.3.1",
"workbox-build": "^6.5.2",
"workbox-core": "^6.5.2",
"workbox-precaching": "^6.5.2"
},
"devDependencies": {
"typescript": "^4.6.3",
"vite": "^2.9.0",
"vite-plugin-pwa": "^0.11.13"
},
"prettier": {
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "crlf",
"bracketSpacing": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -0,0 +1,68 @@
{
"id": "/",
"scope": "/",
"name": "PWA Starter",
"display": "standalone",
"start_url": "/",
"short_name": "starter",
"theme_color": "#E1477E",
"description": "This is a PWA Starter app",
"orientation": "any",
"background_color": "#E1477E",
"related_applications": [],
"prefer_related_applications": false,
"display_override": [
"window-controls-overlay"
],
"icons": [
{
"src": "assets/icons/512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "assets/icons/192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/icons/48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "assets/icons/24x24.png",
"sizes": "24x24",
"type": "image/png"
}
],
"screenshots": [
{
"src": "assets/screenshots/screen.png",
"sizes": "1617x1012",
"type": "image/png"
}
],
"features": [
"Cross Platform",
"fast",
"simple"
],
"categories": [
"utility"
],
"shortcuts": [
{
"name": "Open About",
"short_name": "About",
"description": "Open the about page",
"url": "/about",
"icons": [
{
"src": "assets/icons/192x192.png",
"sizes": "192x192"
}
]
}
]
}

8
frontend/public/sw.js Normal file
View file

@ -0,0 +1,8 @@
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js'
);
// This is your Service Worker, you can put any of your custom Service Worker
// code in this file, above the `precacheAndRoute` line.
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST || []);

91
frontend/src/app-index.ts Normal file
View file

@ -0,0 +1,91 @@
import {css, html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {Router} from '@vaadin/router';
import './pages/app-home';
import './components/header';
import './styles/global.css';
@customElement('app-index')
export class AppIndex extends LitElement {
static get styles() {
return css`
main {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 16px;
}
#routerOutlet > * {
width: 100% !important;
}
#routerOutlet > .leaving {
animation: 160ms fadeOut ease-in-out;
}
#routerOutlet > .entering {
animation: 160ms fadeIn linear;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fadeIn {
from {
opacity: 0.2;
}
to {
opacity: 1;
}
}
`;
}
constructor() {
super();
}
firstUpdated() {
// this method is a lifecycle even in lit
// for more info check out the lit docs https://lit.dev/docs/components/lifecycle/
// For more info on using the @vaadin/router check here https://vaadin.com/router
const router = new Router(this.shadowRoot?.querySelector('#routerOutlet'));
router.setRoutes([
// temporarily cast to any because of a Type bug with the router
{
path: (import.meta as any).env.BASE_URL,
animate: true,
children: [
{path: '', component: 'app-home'},
{
path: 'about',
component: 'app-about',
action: async () => {
await import('./pages/app-about/app-about.js');
},
}
],
} as any,
]);
}
render() {
return html`
<div>
<main>
<div id="routerOutlet"></div>
</main>
</div>
`;
}
}

View file

@ -0,0 +1,80 @@
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
@customElement('app-header')
export class AppHeader extends LitElement {
@property({type: String}) title = 'PWA Starter';
@property({type: Boolean}) enableBack: boolean = false;
static get styles() {
return css`
header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--app-color-primary);
color: white;
height: 4em;
padding-left: 16px;
padding-top: 12px;
position: fixed;
left: env(titlebar-area-x, 0);
top: env(titlebar-area-y, 0);
height: env(titlebar-area-height, 50px);
width: env(titlebar-area-width, 100%);
-webkit-app-region: drag;
}
header h1 {
margin-top: 0;
margin-bottom: 0;
font-size: 20px;
font-weight: bold;
}
nav a {
margin-left: 10px;
}
#back-button-block {
display: flex;
justify-content: space-between;
align-items: center;
width: 12em;
}
@media(prefers-color-scheme: light) {
header {
color: black;
}
nav a {
color: initial;
}
}
`;
}
constructor() {
super();
}
render() {
return html`
<header>
<div id="back-button-block">
${this.enableBack ? html`<sl-button href="${(import.meta as any).env.BASE_URL}">
Back
</sl-button>` : null}
<h1>${this.title}</h1>
</div>
</header>
`;
}
}

View file

@ -0,0 +1,11 @@
import {css} from 'lit';
// these styles can be imported from any component
// for an example of how to use this, check /pages/about-about.ts
export const styles = css`
@media(min-width: 1000px) {
sl-card {
max-width: 70vw;
}
}
`;

View file

@ -0,0 +1,45 @@
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
// You can also import styles from another file
// if you prefer to keep your CSS seperate from your component
import {styles} from './about-styles';
import {styles as sharedStyles} from '../../styles/shared-styles'
import '@shoelace-style/shoelace/dist/components/card/card.js';
@customElement('app-about')
export class AppAbout extends LitElement {
static styles = [
sharedStyles,
styles
]
constructor() {
super();
}
render() {
return html`
<app-header ?enableBack="${true}"></app-header>
<main>
<h2>About Page</h2>
<sl-card>
<h2>Did you know?</h2>
<p>PWAs have access to many useful APIs in modern browsers! These
APIs have enabled many new types of apps that can be built as PWAs, such as advanced graphics editing apps, games,
apps that use machine learning and more!
</p>
<p>Check out <a
href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files">these
docs</a> to learn more about the advanced features that you can use in your PWA</p>
</sl-card>
</main>
`;
}
}

View file

@ -0,0 +1,149 @@
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
// For more info on the @pwabuilder/pwainstall component click here https://github.com/pwa-builder/pwa-install
import '@pwabuilder/pwainstall';
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import {styles} from '../styles/shared-styles';
@customElement('app-home')
export class AppHome extends LitElement {
// For more information on using properties and state in lit
// check out this link https://lit.dev/docs/components/properties/
@property() message = 'Welcome!';
static get styles() {
return [
styles,
css`
#welcomeBar {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#welcomeCard,
#infoCard {
padding: 18px;
padding-top: 0px;
}
pwa-install {
position: absolute;
bottom: 16px;
right: 16px;
}
sl-card::part(footer) {
display: flex;
justify-content: flex-end;
}
@media(min-width: 750px) {
sl-card {
width: 70vw;
}
}
@media (horizontal-viewport-segments: 2) {
#welcomeBar {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
#welcomeCard {
margin-right: 64px;
}
}
`];
}
constructor() {
super();
}
async firstUpdated() {
// this method is a lifecycle even in lit
// for more info check out the lit docs https://lit.dev/docs/components/lifecycle/
console.log('This is your home page');
}
share() {
if ((navigator as any).share) {
(navigator as any).share({
title: 'PWABuilder pwa-starter',
text: 'Check out the PWABuilder pwa-starter!',
url: 'https://github.com/pwa-builder/pwa-starter',
});
}
}
render() {
return html`
<app-header></app-header>
<main>
<div id="welcomeBar">
<sl-card id="welcomeCard">
<div slot="header">
<h2>${this.message}</h2>
</div>
<p>
For more information on the PWABuilder pwa-starter, check out the
<a href="https://github.com/pwa-builder/pwa-starter/wiki/Getting-Started">
Documentation on Github</a>.
</p>
<p id="mainInfo">
Welcome to the
<a href="https://pwabuilder.com">PWABuilder</a>
pwa-starter! Be sure to head back to
<a href="https://pwabuilder.com">PWABuilder</a>
when you are ready to ship this PWA to the Microsoft Store, Google Play
and the Apple App Store!
</p>
${'share' in navigator
? html`<sl-button slot="footer" variant="primary" @click="${this.share}">Share this Starter!</sl-button>`
: null}
</sl-card>
<sl-card id="infoCard">
<h2>Technology Used</h2>
<ul>
<li>
<a href="https://www.typescriptlang.org/">TypeScript</a>
</li>
<li>
<a href="https://lit.dev">lit</a>
</li>
<li>
<a href="https://shoelace.style/">Shoelace</a>
</li>
<li>
<a href="https://vaadin.github.io/vaadin-router/vaadin-router/demo/#vaadin-router-getting-started-demos"
>Vaadin Router</a>
</li>
</ul>
</sl-card>
<sl-button href="${(import.meta as any).env.BASE_URL}about" variant="primary">Navigate to About</sl-button>
</div>
<pwa-install>Install PWA Starter</pwa-install>
</main>
`;
}
}

View file

@ -0,0 +1,31 @@
/*
This file is used for all of your global styles and CSS variables.
Check here https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties for more info on using CSS variables.
*/
:root {
--font-family: sans-serif;
}
html, body {
font-family: var(--font-family);
padding: 0;
margin: 0;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: #181818;
color: white;
}
}
@media (prefers-color-scheme: light) {
html,
body {
background-color: white;
color: black;
}
}

View file

@ -0,0 +1,15 @@
import {css} from 'lit';
// these styles can be imported from any component
// for an example of how to use this, check /pages/about-about.ts
export const styles = css`
@media(min-width: 1000px) {
sl-card {
max-width: 70vw;
}
}
main {
margin-top: 80px;
}
`;

View file

@ -0,0 +1,12 @@
{
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
"configurations": {
"pwa-starter": {
"appLocation": ".",
"outputLocation": "dist",
"appBuildCommand": "npm run build --if-present",
"run": "npm start",
"appDevserverUrl": "http://localhost:3000"
}
}
}

35
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,35 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": [
"es2017",
"esnext",
"dom",
"dom.iterable"
],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false,
"isolatedModules": true,
"types": [
"vite-plugin-pwa/client"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}

28
frontend/vite.config.ts Normal file
View file

@ -0,0 +1,28 @@
import {defineConfig} from 'vite';
import {VitePWA} from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
base: "/",
build: {
sourcemap: true,
assetsDir: "code",
outDir: "build/resources/main/static"
},
plugins: [
VitePWA({
strategies: "injectManifest",
injectManifest: {
swSrc: 'public/sw.js',
swDest: 'build/resources/main/static/sw.js',
globDirectory: 'build/resources/main/static',
globPatterns: [
'**/*.{html,js,css,json, png}',
],
},
devOptions: {
enabled: true
}
})
]
})

View file

@ -7,6 +7,7 @@ kotlinx-coroutines = "1.6.2"
ktor = "2.0.2"
logback = "1.2.11"
mail = "1.6.2"
node-gradle = "3.5.0"
postgres = "42.3.4"
shadow = "7.0.0"
@ -44,4 +45,5 @@ ktor-server = [
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
node-gradle = { id = "com.github.node-gradle.node", version.ref = "node-gradle" }
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }

View file

@ -1,3 +1,2 @@
rootProject.name = "twigs"
include("core", "api", "app", "storage", "db", "web")
include("testhelpers")
include("api", "app", "core", "db", "frontend", "storage", "testhelpers")

2
web/.gitignore vendored
View file

@ -1,2 +0,0 @@
build/
src/main/resources/twigs/

View file

@ -1,51 +0,0 @@
import java.util.*
plugins {
`java-library`
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(kotlin("stdlib"))
api(libs.ktor.server.core)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
tasks.getByName<Test>("test") {
useJUnitPlatform()
}
// TODO: Replace this hack with either a git submodule or an internal Kotlin-based UI
tasks.register("package") {
doLast {
val built = File(rootProject.rootDir.parent, "twigs-web/dist/twigs")
if (built.exists()) {
built.deleteRecursively()
}
val dest = File(project.projectDir, "src/main/resources/twigs")
if (dest.exists()) {
dest.deleteRecursively()
}
var command = listOf(
"cd", "../../twigs-web", ";",
"npm", "i", ";",
"npm", "run", "package"
)
command = if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows")) {
listOf("powershell", "-Command") + command
} else {
listOf("bash", "-c", "\"${command.joinToString(" ")}\"")
}
exec {
commandLine(command)
}
if (!built.copyRecursively(dest, true) || !dest.isDirectory) {
throw GradleException("Failed to copy files from ${built.absolutePath} to ${dest.absolutePath}")
}
}
}
//tasks.getByName("processResources") {
// dependsOn.add("package")
//}