initial pass on frontend: using tailwindcss + daisyui + litelement with webpack build + dev server
This commit is contained in:
parent
c38e0b7bf7
commit
666becdb65
258
frontend/dist/main.js
vendored
Normal file
258
frontend/dist/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
frontend/index.html
Normal file
11
frontend/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Demo</title>
|
||||
<script src="/main.js"></script>
|
||||
</head>
|
||||
<body class="min-w-screen min-h-screen">
|
||||
<browsertrix-app class="flex flex-col min-h-screen bg-blue-400"></browsertrix-app>
|
||||
</body>
|
||||
</html>
|
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.22.0",
|
||||
"daisyui": "^1.14.2",
|
||||
"lit": "^2.0.0",
|
||||
"lit-element-router": "^2.0.3",
|
||||
"path-parser": "^6.1.0",
|
||||
"tailwindcss": "^2.2.16"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"build-dev": "webpack --mode development",
|
||||
"start-dev": "webpack serve --mode=development"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.3.6",
|
||||
"css-loader": "^6.3.0",
|
||||
"postcss": "^8.3.8",
|
||||
"postcss-loader": "^6.1.1",
|
||||
"style-loader": "^3.3.0",
|
||||
"webpack": "^5.56.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.3.0"
|
||||
}
|
||||
}
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
]
|
||||
};
|
345
frontend/src/index.js
Normal file
345
frontend/src/index.js
Normal file
@ -0,0 +1,345 @@
|
||||
import { LiteElement, APIRouter, html } from "./utils";
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class App extends LiteElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.authState = null;
|
||||
|
||||
const authState = window.localStorage.getItem("authState");
|
||||
if (authState) {
|
||||
this.authState = JSON.parse(authState);
|
||||
}
|
||||
|
||||
this.router = new APIRouter({
|
||||
"home": "/",
|
||||
"login": "/log-in",
|
||||
"my-account": "/my-account",
|
||||
"archive-info": "/archive/:aid",
|
||||
"archive-info-tab": "/archive/:aid/:tab"
|
||||
});
|
||||
|
||||
this.viewState = this.router.match(window.location.pathname);
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
viewState: { type: Object },
|
||||
authState: { type: Object }
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
window.addEventListener("popstate", (event) => {
|
||||
// if (event.state.view) {
|
||||
// this.view = event.state.view;
|
||||
// }
|
||||
this.viewState = this.router.match(window.location.pathname);
|
||||
});
|
||||
|
||||
this.viewState = this.router.match(window.location.pathname);
|
||||
}
|
||||
|
||||
navigate(newView) {
|
||||
if (newView.startsWith("http")) {
|
||||
newView = new URL(newView).pathname;
|
||||
}
|
||||
this.viewState = this.router.match(newView);
|
||||
if (this.viewState._route === "login") {
|
||||
this.clearAuthState();
|
||||
}
|
||||
//console.log(this.view._route, window.location.href);
|
||||
window.history.pushState(this.viewState, "", this.viewState._path);
|
||||
}
|
||||
|
||||
navLink(event) {
|
||||
event.preventDefault();
|
||||
this.navigate(event.currentTarget.href);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.renderNavBar()}
|
||||
<div class="w-full h-full px-12 py-12">
|
||||
${this.renderPage()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderNavBar() {
|
||||
return html`
|
||||
<div class="navbar shadow-lg bg-neutral text-neutral-content">
|
||||
<div class="flex-1 px-2 mx-2">
|
||||
<a href="/" class="link link-hover text-lg font-bold" @click="${this.navLink}">Browsertrix Cloud</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
${this.authState ? html`
|
||||
<a class="link link-hover font-bold px-4" href="/my-account" @click="${this.navLink}">My Account</a>
|
||||
<button class="btn btn-error" @click="${this.onLogOut}">Log Out</button>`
|
||||
: html`
|
||||
<button class="btn ${this.viewState._route !== "login" ? "btn-primary" : "btn-ghost"}" @click="${this.onNeedLogin}">Log In</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderPage() {
|
||||
switch (this.viewState._route) {
|
||||
case "login":
|
||||
return html`<log-in @logged-in="${this.onLoggedIn}">`;
|
||||
|
||||
case "home":
|
||||
return html`<div>Home</div>`;
|
||||
|
||||
case "my-account":
|
||||
return html`<my-account @navigate="${this.onNavigateTo}" @need-login="${this.onNeedLogin}" .authState="${this.authState}"></my-account>`;
|
||||
|
||||
case "archive-info":
|
||||
case "archive-info-tab":
|
||||
return html`<btrix-archive @navigate="${this.onNavigateTo}" .authState="${this.authState}" .viewState="${this.viewState}" aid="${this.viewState.aid}" tab="${this.viewState.tab || "running"}"></btrix-archive>`;
|
||||
|
||||
default:
|
||||
return html`<div>Not Found!</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
onLogOut() {
|
||||
this.clearAuthState();
|
||||
this.navigate("/");
|
||||
}
|
||||
|
||||
onLoggedIn(event) {
|
||||
this.authState = {
|
||||
username: event.detail.username,
|
||||
headers: {"Authorization": event.detail.auth}
|
||||
};
|
||||
window.localStorage.setItem("authState", JSON.stringify(this.authState));
|
||||
this.navigate("/my-account");
|
||||
}
|
||||
|
||||
onNeedLogin() {
|
||||
this.clearAuthState();
|
||||
this.navigate("/log-in");
|
||||
}
|
||||
|
||||
onNavigateTo(event) {
|
||||
this.navigate(event.detail);
|
||||
}
|
||||
|
||||
clearAuthState() {
|
||||
this.authState = null;
|
||||
window.localStorage.setItem("authState", "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class LogIn extends LiteElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.loginError = "";
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
loginError: { type: String }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="hero min-h-screen bg-blue-400">
|
||||
<div class="text-center hero-content bg-base-200 shadow-2xl rounded-xl px-16 py-8">
|
||||
|
||||
<div class="max-w-md">
|
||||
<form action="" @submit="${this.onSubmit}">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">User</span>
|
||||
</label>
|
||||
<input id="username" name="username" type="text" placeholder="Username" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Password</span>
|
||||
</label>
|
||||
<input id="password" name="password" type="password" placeholder="Password" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control py-4">
|
||||
<button class="btn btn-primary" type="submit">Log In</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="login-error" class="text-red-600">
|
||||
${this.loginError}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async onSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = this.querySelector("#username").value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set("grant_type", "password");
|
||||
params.set("username", username);
|
||||
params.set("password", this.querySelector("#password").value);
|
||||
|
||||
const headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
const resp = await fetch("/api/auth/jwt/login", {headers, method: "POST", body: params.toString()});
|
||||
if (resp.status !== 200) {
|
||||
this.loginError = "Sorry, invalid credentials";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await resp.json();
|
||||
if (data.token_type === "bearer" && data.access_token) {
|
||||
const auth = "Bearer " + data.access_token;
|
||||
const detail = {auth, username};
|
||||
this.dispatchEvent(new CustomEvent("logged-in", {detail}));
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
|
||||
if (!this.auth) {
|
||||
this.loginError = "Unknown login response";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class MyAccount extends LiteElement
|
||||
{
|
||||
constructor() {
|
||||
super();
|
||||
this.archiveList = [];
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
authState: { type: Object },
|
||||
archiveList: { type: Array },
|
||||
id: { type: String }
|
||||
}
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
if (!this.authState) {
|
||||
this.dispatchEvent(new CustomEvent("need-login"));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.apiFetch("/archives", this.authState);
|
||||
this.archiveList = data.archives;
|
||||
|
||||
const data2 = await this.apiFetch("/users/me", this.authState);
|
||||
this.id = data2.id;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container bg-base-200 m-auto border rounded-lg px-8 py-8">
|
||||
<h2 class="text-2xl font-bold">Your Archives</h2>
|
||||
${this.archiveList.map(archive => html`
|
||||
<div class="card mt-6 ml-6 border rounded-none border-gray-600 hover:bg-gray-300">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<span class="mr-4">${archive.name}</span>${this.getAccessValue(archive)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-primary" href="/archive/${archive.id}" @click="${this.navLink}">View Archive</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getAccessValue(archive) {
|
||||
const value = archive.users && archive.users[this.id];
|
||||
switch (value) {
|
||||
case 40:
|
||||
return html`<div class="badge badge-info">Owner</div>`;
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
class Archive extends LiteElement
|
||||
{
|
||||
static get properties() {
|
||||
return {
|
||||
authState: { type: Object },
|
||||
aid: {type: String},
|
||||
tab: {type: String},
|
||||
viewState: { type: Object }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const aid = this.aid;
|
||||
const tab = this.tab || "running";
|
||||
return html`
|
||||
<div class="container bg-base-200 m-auto border shadow-xl rounded-lg px-8 py-8">
|
||||
<div class="tabs tabs-boxed">
|
||||
<a href="/archive/${aid}/running" class="tab ${tab === "running" ? 'tab-active' : ''}" @click="${this.navLink}">Crawls Running</a>
|
||||
<a href="/archive/${aid}/finished" class="tab ${tab === "finished" ? 'tab-active' : ''}" @click="${this.navLink}">Finished</a>
|
||||
<a href="/archive/${aid}/configs" class="tab ${tab === "configs" ? 'tab-active' : ''}" @click="${this.navLink}">Crawl Configs</a>
|
||||
</div>
|
||||
${tab === "configs" ?
|
||||
html`<btrix-archive-configs .archive=${this}></btrix-archive-configs>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
class ArchiveConfigs extends LiteElement
|
||||
{
|
||||
static get properties() {
|
||||
return {
|
||||
archive: { type: Object },
|
||||
configs: { type: Array }
|
||||
}
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
const res = await this.apiFetch(`/archives/${this.archive.aid}/crawlconfigs`, this.archive.authState);
|
||||
this.configs = res.crawl_configs;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>Archive Configs!</div>
|
||||
${this.configs && this.configs.map((config) => html`
|
||||
<div>${config.crawlCount} ${config.config.seeds}</div>
|
||||
`)}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
customElements.define("browsertrix-app", App);
|
||||
customElements.define("log-in", LogIn);
|
||||
customElements.define("my-account", MyAccount);
|
||||
customElements.define("btrix-archive", Archive);
|
||||
customElements.define("btrix-archive-configs", ArchiveConfigs);
|
||||
|
62
frontend/src/utils.js
Normal file
62
frontend/src/utils.js
Normal file
@ -0,0 +1,62 @@
|
||||
import "tailwindcss/tailwind.css";
|
||||
|
||||
import { LitElement, html } from "lit";
|
||||
import { Path } from "path-parser";
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
export class LiteElement extends LitElement
|
||||
{
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
navTo(url) {
|
||||
this.dispatchEvent(new CustomEvent("navigate", {detail: url}));
|
||||
}
|
||||
|
||||
navLink(event) {
|
||||
event.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent("navigate", {detail: event.currentTarget.href, bubbles: true, composed: true}));
|
||||
}
|
||||
|
||||
async apiFetch(path, auth) {
|
||||
const resp = await fetch("/api" + path, {headers: auth.headers});
|
||||
if (resp.status !== 200) {
|
||||
this.navTo("/log-in");
|
||||
throw new Error("logged out");
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
export class APIRouter {
|
||||
constructor(paths) {
|
||||
this.routes = {};
|
||||
|
||||
for (const [name, route] of Object.entries(paths)) {
|
||||
this.routes[name] = new Path(route);
|
||||
}
|
||||
}
|
||||
|
||||
match(path) {
|
||||
for (const [name, route] of Object.entries(this.routes)) {
|
||||
const parts = path.split("?", 2);
|
||||
const matchUrl = parts[0];
|
||||
|
||||
const res = route.test(matchUrl);
|
||||
if (res) {
|
||||
res._route = name;
|
||||
res._path = path;
|
||||
//res._query = new URLSearchParams(parts.length === 2 ? parts[1] : "");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return {_route: null, _path: path};
|
||||
}
|
||||
}
|
||||
|
||||
export { html };
|
18
frontend/tailwind.config.js
Normal file
18
frontend/tailwind.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
|
||||
purge: {
|
||||
content: ['./*.html', './src/*.js'],
|
||||
options: {
|
||||
safelist: [
|
||||
/data-theme$/,
|
||||
]
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('daisyui')
|
||||
],
|
||||
extract: {
|
||||
include: ['./src/*.js'],
|
||||
},
|
||||
};
|
49
frontend/webpack.config.js
Normal file
49
frontend/webpack.config.js
Normal file
@ -0,0 +1,49 @@
|
||||
// webpack.config.js
|
||||
const path = require("path")
|
||||
|
||||
|
||||
const backendUrl = new URL("http://btrix.cloud/");
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "main.js",
|
||||
publicPath: "/"
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
{ loader: "css-loader", options: { importLoaders: 1 } },
|
||||
"postcss-loader",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
watchFiles: ["src/*.js"],
|
||||
open: true,
|
||||
compress: true,
|
||||
hot: true,
|
||||
static: {
|
||||
directory: path.join(__dirname),
|
||||
//publicPath: "/",
|
||||
watch: true
|
||||
},
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: backendUrl.href,
|
||||
headers: {
|
||||
'Host': backendUrl.host
|
||||
},
|
||||
pathRewrite: { '^/api': '' },
|
||||
},
|
||||
},
|
||||
port: 9870
|
||||
},
|
||||
}
|
2926
frontend/yarn.lock
Normal file
2926
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user