Skip to content
This repository has been archived by the owner on Dec 17, 2024. It is now read-only.

Commit

Permalink
implement new webgen changes
Browse files Browse the repository at this point in the history
  • Loading branch information
greg6775 committed Jan 14, 2024
1 parent cee37f6 commit a5ad032
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 121 deletions.
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -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/[email protected]/",
"shared/": "./pages/shared/"
Expand Down
2 changes: 1 addition & 1 deletion pages/_legacy/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions pages/hosting/views/profile.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -137,7 +137,7 @@ export const profileView = () =>
);

type ShopVariant =
{ type: 'available' | 'recommended' | 'blocked', label: Referenceable<string>, sublabel: Referenceable<string>, action: (env: MouseEvent) => Promise<void>; };
{ type: 'available' | 'recommended' | 'blocked', label: Refable<string>, sublabel: Refable<string>, action: (env: MouseEvent) => Promise<void>; };

const ShopStack = (actionText: string, variant: ShopVariant) => Grid(
Label(actionText),
Expand Down
8 changes: 4 additions & 4 deletions pages/hosting/views/table2.ts
Original file line number Diff line number Diff line change
@@ -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<Data> = {
converter: (data: Data) => Component;
Expand Down Expand Up @@ -54,7 +54,7 @@ export class Table2<Data> extends Component {
).addClass("wgtable")).asRefComponent().draw());
}

setColumnTemplate(layout: Referenceable<string>) {
setColumnTemplate(layout: Refable<string>) {
asRef(layout).listen(value => {
this.wrapper.style.setProperty("--wgtable-column-template", value);
});
Expand All @@ -70,12 +70,12 @@ export class Table2<Data> extends Component {
return this;
}

setRowClickEnabled(clickableHandler: Referenceable<RowClickEnabledHandler>) {
setRowClickEnabled(clickableHandler: Refable<RowClickEnabledHandler>) {
asRef(clickableHandler).listen(value => this.rowClickable.setValue(value));
return this;
}

setRowClick(clickHandler: Referenceable<RowClickHandler>) {
setRowClick(clickHandler: Refable<RowClickHandler>) {
asRef(clickHandler).listen(value => this.rowClick.setValue(value));
return this;
}
Expand Down
4 changes: 2 additions & 2 deletions pages/shared/Progress.ts
Original file line number Diff line number Diff line change
@@ -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<number>) {
export function Progress(progress: Refable<number>) {
return Box(
Custom((() => {
if (progress == -1) return Label("⚠️ Failed to upload!").addClass("error-message").setTextSize("sm").draw();
Expand Down
4 changes: 2 additions & 2 deletions pages/shared/list.ts
Original file line number Diff line number Diff line change
@@ -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 = <T>(items: Referenceable<External<T[]> | 'loading' | T[]>, map: (val: T) => Component) => new class extends Component {
export const HeavyList = <T>(items: Refable<External<T[]> | 'loading' | T[]>, map: (val: T) => Component) => new class extends Component {
placeholder = Box();
loadMore = async (_offset: number, _limit: number) => { };
paging = asState({ enabled: false, limit: 30 });
Expand Down
18 changes: 9 additions & 9 deletions pages/shared/navigation.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,12 +11,12 @@ export type RenderItem = Component | MenuNode;

export interface MenuNode {
id: string;
hidden?: Referenceable<boolean>;
title: Referenceable<string>;
subtitle?: Referenceable<string>;
children?: Referenceable<RenderItem[]>;
replacement?: Referenceable<Component>;
suffix?: Referenceable<Component>;
hidden?: Refable<boolean>;
title: Refable<string>;
subtitle?: Refable<string>;
children?: Refable<RenderItem[]>;
replacement?: Refable<Component>;
suffix?: Refable<Component>;
clickHandler?: ClickHandler;
firstRenderHandler?: ClickHandler;
}
Expand All @@ -27,7 +27,7 @@ export interface CategoryNode extends MenuNode {

export type RootNode = Omit<MenuNode, "id"> & {
categories?: CategoryNode[];
actions?: Referenceable<Component[]>;
actions?: Refable<Component[]>;
};

function traverseToMenuNode(rootNode: RootNode, path: string): MenuNode | null {
Expand Down Expand Up @@ -162,7 +162,7 @@ class MenuImpl extends Component {
}

/**
* A Extendable Declarative Referenceable Navigation Component.
* A Extendable Declarative Refable Navigation Component.
* @param rootNode
* @returns
*/
Expand Down
2 changes: 1 addition & 1 deletion pages/shared/restSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
140 changes: 77 additions & 63 deletions pages/user/settings.personal.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,83 @@
import { ZodError } from "https://deno.land/x/[email protected]/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 = <AdvancedImage>{ 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 = <AdvancedImage>{ type: "waiting-upload", filename: file.name, blobUrl };
},
backendResponse: () => {
data.loading = false;
data.profilePicture = <AdvancedImage>{ type: "direct", source: async () => await file };
},
credentials: () => API.getToken(),
onUploadTick: async (percentage) => {
data.profilePicture = <AdvancedImage>{ 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: <ZodError | undefined>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 = <AdvancedImage>{ 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 = <AdvancedImage>{ type: "waiting-upload", filename: file.name, blobUrl };
},
backendResponse: () => {
state.loading = false;
state.profilePicture = <AdvancedImage>{ type: "direct", source: async () => await file };
},
credentials: () => API.getToken(),
onUploadTick: async (percentage) => {
state.profilePicture = <AdvancedImage>{ 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");
};
77 changes: 44 additions & 33 deletions pages/user/settings.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { ZodError } from "https://deno.land/x/[email protected]/ZodError.ts";
import zod from "https://deno.land/x/[email protected]/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: <string | undefined>undefined,
verifyNewPassword: <string | undefined>undefined,
validationState: <ZodError | undefined>undefined,
});

export const settingsMenu = Navigation({
const settingsMenu = Navigation({
title: "Settings",
children: [
{
Expand All @@ -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"),
]
},
{
Expand Down
Loading

0 comments on commit a5ad032

Please sign in to comment.