diff --git a/deno.jsonc b/deno.jsonc index 7df1b31..5b4a9d5 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "imports": { - "webgen/": "https://raw.githubusercontent.com/lucsoft/WebGen/de5703e/", + "webgen/": "https://raw.githubusercontent.com/lucsoft/WebGen/ddee494/", // "webgen/": "../WebGen/", "std/": "https://deno.land/std@0.212.0/", "shared/": "./pages/shared/" diff --git a/pages/_legacy/helper.ts b/pages/_legacy/helper.ts index 3f28bb8..3ce83be 100644 --- a/pages/_legacy/helper.ts +++ b/pages/_legacy/helper.ts @@ -32,7 +32,7 @@ export type ProfileData = { export function IsLoggedIn(): ProfileData | null { try { - return localStorage[ "access-token" ] ? JSON.parse(b64DecodeUnicode(localStorage[ "access-token" ]?.split(".")[ 1 ])).user : null; + return localStorage[ "access-token" ] ? JSON.parse(b64DecodeUnicode(localStorage[ "access-token" ].split(".")[ 1 ])).user : null; } catch (_) { // Invalid state. We gonna need to say goodbye to that session resetTokens(); diff --git a/pages/hosting/views/profile.ts b/pages/hosting/views/profile.ts index b60c5eb..e5d521e 100644 --- a/pages/hosting/views/profile.ts +++ b/pages/hosting/views/profile.ts @@ -1,7 +1,7 @@ import { confettiFromElement } from "shared/libs/canvasConfetti.ts"; import { API, stupidErrorAlert } from "shared/mod.ts"; import { format } from "std/fmt/bytes.ts"; -import { Box, Button, ButtonStyle, Color, Empty, Entry, Grid, Label, MediaQuery, Referenceable, Vertical } from "webgen/mod.ts"; +import { Box, Button, ButtonStyle, Color, Empty, Entry, Grid, Label, MediaQuery, Refable, Vertical } from "webgen/mod.ts"; import { MB, state } from "../data.ts"; import { refreshState } from "../loading.ts"; import './profile.css'; @@ -137,7 +137,7 @@ export const profileView = () => ); type ShopVariant = - { type: 'available' | 'recommended' | 'blocked', label: Referenceable, sublabel: Referenceable, action: (env: MouseEvent) => Promise; }; + { type: 'available' | 'recommended' | 'blocked', label: Refable, sublabel: Refable, action: (env: MouseEvent) => Promise; }; const ShopStack = (actionText: string, variant: ShopVariant) => Grid( Label(actionText), diff --git a/pages/hosting/views/table2.ts b/pages/hosting/views/table2.ts index cee8177..8b89358 100644 --- a/pages/hosting/views/table2.ts +++ b/pages/hosting/views/table2.ts @@ -1,4 +1,4 @@ -import { Box, Component, Custom, Label, Reference, Referenceable, asRef, refMerge } from "webgen/mod.ts"; +import { Box, Component, Custom, Label, Refable, Reference, asRef, refMerge } from "webgen/mod.ts"; export type TableColumn = { converter: (data: Data) => Component; @@ -54,7 +54,7 @@ export class Table2 extends Component { ).addClass("wgtable")).asRefComponent().draw()); } - setColumnTemplate(layout: Referenceable) { + setColumnTemplate(layout: Refable) { asRef(layout).listen(value => { this.wrapper.style.setProperty("--wgtable-column-template", value); }); @@ -70,12 +70,12 @@ export class Table2 extends Component { return this; } - setRowClickEnabled(clickableHandler: Referenceable) { + setRowClickEnabled(clickableHandler: Refable) { asRef(clickableHandler).listen(value => this.rowClickable.setValue(value)); return this; } - setRowClick(clickHandler: Referenceable) { + setRowClick(clickHandler: Refable) { asRef(clickHandler).listen(value => this.rowClick.setValue(value)); return this; } diff --git a/pages/shared/Progress.ts b/pages/shared/Progress.ts index d474d44..9bd71e2 100644 --- a/pages/shared/Progress.ts +++ b/pages/shared/Progress.ts @@ -1,6 +1,6 @@ -import { Box, createElement, Custom, isRef, Label, Referenceable } from "webgen/mod.ts"; +import { Box, createElement, Custom, isRef, Label, Refable } from "webgen/mod.ts"; -export function Progress(progress: Referenceable) { +export function Progress(progress: Refable) { return Box( Custom((() => { if (progress == -1) return Label("⚠️ Failed to upload!").addClass("error-message").setTextSize("sm").draw(); diff --git a/pages/shared/list.ts b/pages/shared/list.ts index 5c958ac..b5cc573 100644 --- a/pages/shared/list.ts +++ b/pages/shared/list.ts @@ -1,9 +1,9 @@ -import { Box, Button, CenterV, Component, Empty, Horizontal, Label, MIcon, Referenceable, Vertical, asRef, asState } from "webgen/mod.ts"; +import { Box, Button, CenterV, Component, Empty, Horizontal, Label, MIcon, Refable, Reference, Vertical, asRef, asState } from "webgen/mod.ts"; import { LoadingSpinner } from "./components.ts"; import { External, displayError } from "./restSpec.ts"; // TODO: don't rerender the complete list on update. virtual list? -export const HeavyList = (items: Referenceable | 'loading' | T[]>, map: (val: T) => Component) => new class extends Component { +export const HeavyList = (items: Refable | 'loading' | T[]>, map: (val: T) => Component) => new class extends Component { placeholder = Box(); loadMore = async (_offset: number, _limit: number) => { }; paging = asState({ enabled: false, limit: 30 }); diff --git a/pages/shared/navigation.ts b/pages/shared/navigation.ts index 52db1ea..91b645d 100644 --- a/pages/shared/navigation.ts +++ b/pages/shared/navigation.ts @@ -1,5 +1,5 @@ import { assert } from "std/assert/assert.ts"; -import { Box, Component, Empty, Entry, Grid, Label, MIcon, Reference, Referenceable, Taglist, Vertical, asRef, isMobile, isRef } from "webgen/mod.ts"; +import { Box, Component, Empty, Entry, Grid, Label, MIcon, Refable, Reference, Taglist, Vertical, asRef, isMobile, isRef } from "webgen/mod.ts"; import { HeavyList } from "./list.ts"; import './navigation.css'; @@ -11,12 +11,12 @@ export type RenderItem = Component | MenuNode; export interface MenuNode { id: string; - hidden?: Referenceable; - title: Referenceable; - subtitle?: Referenceable; - children?: Referenceable; - replacement?: Referenceable; - suffix?: Referenceable; + hidden?: Refable; + title: Refable; + subtitle?: Refable; + children?: Refable; + replacement?: Refable; + suffix?: Refable; clickHandler?: ClickHandler; firstRenderHandler?: ClickHandler; } @@ -27,7 +27,7 @@ export interface CategoryNode extends MenuNode { export type RootNode = Omit & { categories?: CategoryNode[]; - actions?: Referenceable; + actions?: Refable; }; function traverseToMenuNode(rootNode: RootNode, path: string): MenuNode | null { @@ -162,7 +162,7 @@ class MenuImpl extends Component { } /** - * A Extendable Declarative Referenceable Navigation Component. + * A Extendable Declarative Refable Navigation Component. * @param rootNode * @returns */ diff --git a/pages/shared/restSpec.ts b/pages/shared/restSpec.ts index 5eae289..cda435d 100644 --- a/pages/shared/restSpec.ts +++ b/pages/shared/restSpec.ts @@ -7,7 +7,7 @@ export const Permissions = [ "/hmsys/user/manage", "/bbn", - "/bbn/beta-hosting", + "/bbn/hosting", "/bbn/manage", "/bbn/manage/drops", "/bbn/manage/drops/review", diff --git a/pages/user/settings.personal.ts b/pages/user/settings.personal.ts index 793b891..37de239 100644 --- a/pages/user/settings.personal.ts +++ b/pages/user/settings.personal.ts @@ -1,69 +1,83 @@ +import { ZodError } from "https://deno.land/x/zod@v3.22.4/mod.ts"; import { API, StreamingUploadHandler, stupidErrorAlert } from "shared/mod.ts"; import { delay } from "std/async/mod.ts"; -import { AdvancedImage, Box, Grid, IconButton, Image, MIcon, TextInput, Vertical, createFilePicker } from "webgen/mod.ts"; +import { AdvancedImage, Box, Button, CenterV, Empty, Grid, Horizontal, IconButton, Image, Label, MIcon, Spacer, TextInput, Validate, Vertical, asState, createFilePicker, getErrorMessage } from "webgen/mod.ts"; +import { zod } from "webgen/zod.ts"; import { activeUser, allowedImageFormats, forceRefreshToken } from "../_legacy/helper.ts"; export function ChangePersonal() { - return Wizard({ - submitAction: async ([ { data: { data } } ]) => { - await API.user.setMe.post(data) - .then(stupidErrorAlert); - await forceRefreshToken(); - }, - buttonArrangement: "flex-end", - buttonAlignment: "top", - }, () => [ - Page({ - email: activeUser.email, - name: activeUser.username, - loading: false, - profilePicture: activeUser.avatar ?? { type: "loading" } as string | AdvancedImage | undefined - }, (data) => [ - Vertical( - Grid( - data.$profilePicture.map(() => Box(Image(data.profilePicture ?? { type: "loading" }, "Your Avatarimage"), IconButton(MIcon("edit"), "edit-icon")).addClass("upload-image").onClick(async () => { - const file = await createFilePicker(allowedImageFormats.join(",")); - const blobUrl = URL.createObjectURL(file); - data.profilePicture = { type: "uploading", filename: file.name, blobUrl, percentage: 0 }; - data.loading = true; - setTimeout(() => { - StreamingUploadHandler(`user/set-me/avatar/upload`, { - failure: () => { - data.loading = false; - data.profilePicture = activeUser.avatar; - alert("Your Upload has failed. Please try a different file or try again later"); - }, - uploadDone: () => { - data.profilePicture = { type: "waiting-upload", filename: file.name, blobUrl }; - }, - backendResponse: () => { - data.loading = false; - data.profilePicture = { type: "direct", source: async () => await file }; - }, - credentials: () => API.getToken(), - onUploadTick: async (percentage) => { - data.profilePicture = { type: "uploading", filename: file.name, blobUrl, percentage }; - await delay(2); - } - }, file); - }); + const state = asState({ + email: activeUser.email, + name: activeUser.username, + loading: false, + profilePicture: activeUser.avatar ?? { type: "loading" } as string | AdvancedImage | undefined, + validationState: undefined, + }); - })).asRefComponent(), - [ - { width: 2 }, - Vertical( - TextInput("text", "Name").sync(data, "name"), - TextInput("email", "Email").sync(data, "email") - ).setGap("20px") - ] - ) - .setDynamicColumns(1, "12rem") - .addClass("settings-form") - .setGap("15px") - ).setGap("20px").addClass("limited-width"), - ]).setValidator((v) => v.object({ - name: v.string().min(2), - email: v.string().email() - }).strip()) - ]); -} + return Vertical( + Grid( + state.$profilePicture.map(profilePicture => Box(Image(profilePicture ?? { type: "loading" }, "Your Avatarimage"), IconButton(MIcon("edit"), "edit-icon")).addClass("upload-image").onClick(async () => { + const file = await createFilePicker(allowedImageFormats.join(",")); + const blobUrl = URL.createObjectURL(file); + profilePicture = { type: "uploading", filename: file.name, blobUrl, percentage: 0 }; + state.loading = true; + setTimeout(() => { + StreamingUploadHandler(`user/set-me/avatar/upload`, { + failure: () => { + state.loading = false; + state.profilePicture = activeUser.avatar; + alert("Your Upload has failed. Please try a different file or try again later"); + }, + uploadDone: () => { + state.profilePicture = { type: "waiting-upload", filename: file.name, blobUrl }; + }, + backendResponse: () => { + state.loading = false; + state.profilePicture = { type: "direct", source: async () => await file }; + }, + credentials: () => API.getToken(), + onUploadTick: async (percentage) => { + state.profilePicture = { type: "uploading", filename: file.name, blobUrl, percentage }; + await delay(2); + } + }, file); + }); + + })).asRefComponent(), + [ + { width: 2 }, + Vertical( + TextInput("text", "Name").sync(state, "name"), + TextInput("email", "Email").sync(state, "email") + ).setGap("20px") + ] + ) + .setDynamicColumns(1, "12rem") + .addClass("settings-form") + .setGap("15px"), + Horizontal( + Spacer(), + Box(state.$validationState.map(error => error ? CenterV( + Label(getErrorMessage(error)) + .addClass("error-message") + .setMargin("0 0.5rem 0 0") + ) + : Empty()).asRefComponent()), + Button("Save").onClick(async () => { + const { error, validate } = Validate( + state, + zod.object({ + name: zod.string().min(2), + email: zod.string().email() + }) + ); + + const data = validate(); + if (error.getValue()) return state.validationState = error.getValue(); + if (data) await API.user.setMe.post(state) + .then(stupidErrorAlert); + await forceRefreshToken(); + state.validationState = undefined; + })), + ).setGap("20px").addClass("limited-width"); +}; \ No newline at end of file diff --git a/pages/user/settings.ts b/pages/user/settings.ts index bf1f4bc..f95d076 100644 --- a/pages/user/settings.ts +++ b/pages/user/settings.ts @@ -1,21 +1,23 @@ +import { ZodError } from "https://deno.land/x/zod@v3.22.4/ZodError.ts"; import zod from "https://deno.land/x/zod@v3.22.4/index.ts"; import { API, Navigation } from "shared/mod.ts"; -import { Body, Grid, TextInput, Vertical, WebGen, isMobile } from "webgen/mod.ts"; +import { Body, Box, Button, CenterV, Empty, Grid, Horizontal, Label, Spacer, TextInput, Validate, Vertical, WebGen, asState, getErrorMessage, isMobile } from "webgen/mod.ts"; import '../../assets/css/main.css'; import { DynaNavigation } from "../../components/nav.ts"; import { RegisterAuthRefresh, logOut } from "../_legacy/helper.ts"; import { ChangePersonal } from "./settings.personal.ts"; -WebGen({}); +WebGen(); + await RegisterAuthRefresh(); -const passwordWizard = zod.object({ - newPassword: zod.string({ invalid_type_error: "New password is missing" }).min(4), - verifyNewPassword: zod.string({ invalid_type_error: "Verify New password is missing" }).min(4) -}) - .refine(val => val.newPassword == val.verifyNewPassword, "Your new password didn't match"); +const state = asState({ + newPassword: undefined, + verifyNewPassword: undefined, + validationState: undefined, +}); -export const settingsMenu = Navigation({ +const settingsMenu = Navigation({ title: "Settings", children: [ { @@ -30,32 +32,41 @@ export const settingsMenu = Navigation({ id: "change-password", title: "Change Password", children: [ - Wizard({ - submitAction: async ([ { data: { data } } ]) => { - await API.user.setMe.post({ password: data.newPassword }); - logOut(); - }, - buttonArrangement: "flex-end", - buttonAlignment: "top", - }, () => [ - Page({ - newPassword: undefined, - verifyNewPassword: undefined - }, (data) => [ + Vertical( + Grid([ + { width: 2 }, Vertical( - Grid([ - { width: 2 }, - Vertical( - TextInput("password", "New Password").sync(data, "newPassword"), - TextInput("password", "Verify New Password").sync(data, "verifyNewPassword") - ).setGap("20px") - ]) - .setDynamicColumns(1, "12rem") - .addClass("settings-form") - .setGap("15px") - ).setGap("20px"), - ]).setValidator(() => passwordWizard) - ]) + TextInput("password", "New Password").sync(state, "newPassword"), + TextInput("password", "Verify New Password").sync(state, "verifyNewPassword") + ).setGap("20px") + ]) + .setDynamicColumns(1, "12rem") + .addClass("settings-form") + .setGap("15px"), + Horizontal( + Spacer(), + Box(state.$validationState.map(error => error ? CenterV( + Label(getErrorMessage(error)) + .addClass("error-message") + .setMargin("0 0.5rem 0 0") + ) + : Empty()).asRefComponent()), + Button("Save").onClick(async () => { + const { error, validate } = Validate( + state, + zod.object({ + newPassword: zod.string({ invalid_type_error: "New password is missing" }).min(4), + verifyNewPassword: zod.string({ invalid_type_error: "Verify New password is missing" }).min(4).refine(val => val == state.newPassword, "Your new password didn't match") + }) + ); + + const data = validate(); + if (error.getValue()) return state.validationState = error.getValue(); + if (data) await API.user.setMe.post({ password: data.newPassword }); + logOut(); + state.validationState = undefined; + })), + ).setGap("20px"), ] }, { diff --git a/pages/wallet/wallet.ts b/pages/wallet/wallet.ts index 018c6f1..030aa38 100644 --- a/pages/wallet/wallet.ts +++ b/pages/wallet/wallet.ts @@ -60,20 +60,20 @@ sheetStack.setDefault(Vertical( Button("Request Payout") .onClick(() => { const amount = asState({ value: "0" }); - const sheet = SheetDialog(sheetStack, "Request Payout", + const dialog = SheetDialog(sheetStack, "Request Payout", Grid( Label("How much would you like to withdraw?"), TextInput("text", "Amount").sync(amount, "value"), Button("Request") .onClick(() => { handlePayoutResponse(Number(amount.value)); - sheet.close(); + dialog.close(); }) ) .setGap() .setMargin("10px") ); - sheet.open(); + dialog.open(); }) .setColor(wallet.balance?.unrestrained! + wallet.balance?.restrained! > 10 ? Color.Grayscaled : Color.Disabled) ]