initial public commit

This commit is contained in:
Gani Georgiev
2022-07-07 00:19:05 +03:00
commit 3d07f0211d
484 changed files with 92412 additions and 0 deletions
@@ -0,0 +1,258 @@
<script>
import { createEventDispatcher } from "svelte";
import { slide } from "svelte/transition";
import { Admin } from "pocketbase";
import CommonHelper from "@/utils/CommonHelper";
import ApiClient from "@/utils/ApiClient";
import { setErrors } from "@/stores/errors";
import { confirm } from "@/stores/confirmation";
import { addSuccessToast } from "@/stores/toasts";
import Field from "@/components/base/Field.svelte";
import Toggler from "@/components/base/Toggler.svelte";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
const dispatch = createEventDispatcher();
const formId = "admin_" + CommonHelper.randomString(5);
let panel;
let admin = new Admin();
let isSaving = false;
let confirmClose = false; // prevent close recursion
let avatar = 0;
let email = "";
let password = "";
let passwordConfirm = "";
let changePasswordToggle = false;
$: hasChanges =
(admin.isNew && email != "") ||
changePasswordToggle ||
email !== admin.email ||
avatar !== admin.avatar;
export function show(model) {
load(model);
confirmClose = true;
return panel?.show();
}
export function hide() {
return panel?.hide();
}
function load(model) {
setErrors({}); // reset errors
admin = model?.clone ? model.clone() : new Admin();
reset(); // reset form
}
function reset() {
changePasswordToggle = false;
email = admin?.email || "";
avatar = admin?.avatar || 0;
password = "";
passwordConfirm = "";
}
function save() {
if (isSaving || !hasChanges) {
return;
}
isSaving = true;
const data = { email, avatar };
if (admin.isNew || changePasswordToggle) {
data["password"] = password;
data["passwordConfirm"] = passwordConfirm;
}
let request;
if (admin.isNew) {
request = ApiClient.Admins.create(data);
} else {
request = ApiClient.Admins.update(admin.id, data);
}
request
.then(async (result) => {
confirmClose = false;
hide();
addSuccessToast(admin.isNew ? "Successfully created admin." : "Successfully updated admin.");
dispatch("save", result);
if (ApiClient.AuthStore.model?.id === result.id) {
ApiClient.AuthStore.save(ApiClient.AuthStore.token, result);
}
})
.catch((err) => {
ApiClient.errorResponseHandler(err);
})
.finally(() => {
isSaving = false;
});
}
function deleteConfirm() {
if (!admin?.id) {
return; // nothing to delete
}
confirm(`Do you really want to delete the selected admin?`, () => {
return ApiClient.Admins.delete(admin.id)
.then(() => {
confirmClose = false;
hide();
addSuccessToast("Successfully deleted admin.");
dispatch("delete", admin);
})
.catch((err) => {
ApiClient.errorResponseHandler(err);
});
});
}
</script>
<OverlayPanel
bind:this={panel}
popup
class="admin-panel"
beforeHide={() => {
if (hasChanges && confirmClose) {
confirm("You have unsaved changes. Do you really want to close the panel?", () => {
confirmClose = false;
hide();
});
return false;
}
return true;
}}
on:hide
on:show
>
<svelte:fragment slot="header">
<h4>
{admin.isNew ? "New admin" : "Edit admin"}
</h4>
</svelte:fragment>
<form id={formId} class="grid" autocomplete="off" on:submit|preventDefault={save}>
{#if !admin.isNew}
<Field class="form-field disabled" name="id" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">ID</span>
</label>
<input type="text" id={uniqueId} value={admin.id} disabled />
</Field>
{/if}
<div class="content">
<p class="section-title">Avatar</p>
<div class="flex flex-gap-xs flex-wrap">
{#each [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as index}
<figure
tabindex="0"
class="link-fade thumb thumb-circle {index == avatar ? 'thumb-active' : 'thumb-sm'}"
on:click={() => (avatar = index)}
on:keydown={(e) => {
if (e.code === "Enter" || e.code === "Space") {
e.preventDefault();
avatar = index;
}
}}
>
<img
src="{import.meta.env.BASE_URL}images/avatars/avatar{index}.svg"
alt="Avatar {index}"
/>
</figure>
{/each}
</div>
</div>
<Field class="form-field required" name="email" let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon("email")} />
<span class="txt">Email</span>
</label>
<input type="email" autocomplete="off" id={uniqueId} required bind:value={email} />
</Field>
{#if !admin.isNew}
<Field class="form-field form-field-toggle" let:uniqueId>
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
<label for={uniqueId}>Change password</label>
</Field>
{/if}
{#if admin.isNew || changePasswordToggle}
<div class="col-12">
<div class="grid" transition:slide={{ duration: 150 }}>
<div class="col-sm-6">
<Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={password}
/>
</Field>
</div>
<div class="col-sm-6">
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
<label for={uniqueId}>
<i class="ri-lock-line" />
<span class="txt">Password confirm</span>
</label>
<input
type="password"
autocomplete="new-password"
id={uniqueId}
required
bind:value={passwordConfirm}
/>
</Field>
</div>
</div>
</div>
{/if}
</form>
<svelte:fragment slot="footer">
{#if !admin.isNew}
<button type="button" class="btn btn-sm btn-circle btn-secondary">
<!-- empty span for alignment -->
<span />
<i class="ri-more-line" />
<Toggler class="dropdown dropdown-upside dropdown-left dropdown-nowrap">
<button type="button" class="dropdown-item" on:click={() => deleteConfirm()}>
<i class="ri-delete-bin-7-line" />
<span class="txt">Delete</span>
</button>
</Toggler>
</button>
<div class="flex-fill" />
{/if}
<button type="button" class="btn btn-secondary" disabled={isSaving} on:click={() => hide()}>
<span class="txt">Cancel</span>
</button>
<button
type="submit"
form={formId}
class="btn btn-expanded"
class:btn-loading={isSaving}
disabled={!hasChanges || isSaving}
>
<span class="txt">{admin.isNew ? "Create" : "Save changes"}</span>
</button>
</svelte:fragment>
</OverlayPanel>
@@ -0,0 +1,66 @@
<script>
import { link, replace } from "svelte-spa-router";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { addSuccessToast } from "@/stores/toasts";
import FullPage from "@/components/base/FullPage.svelte";
import Field from "@/components/base/Field.svelte";
export let params;
let newPassword = "";
let newPasswordConfirm = "";
let isLoading = false;
$: email = CommonHelper.getJWTPayload(params?.token).email || "";
async function submit() {
if (isLoading) {
return;
}
isLoading = true;
try {
await ApiClient.Admins.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
addSuccessToast("Successfully set a new admin password.");
replace("/");
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoading = false;
}
</script>
<FullPage>
<form class="m-b-base" on:submit|preventDefault={submit}>
<div class="content txt-center m-b-sm">
<h4 class="m-b-xs">
Reset your admin password
{#if email}
for <strong class="txt-nowrap">{email}</strong>
{/if}
</h4>
</div>
<Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}>New password</label>
<!-- svelte-ignore a11y-autofocus -->
<input type="password" id={uniqueId} required autofocus bind:value={newPassword} />
</Field>
<Field class="form-field required" name="passwordConfirm" let:uniqueId>
<label for={uniqueId}>New password confirm</label>
<input type="password" id={uniqueId} required bind:value={newPasswordConfirm} />
</Field>
<button type="submit" class="btn btn-lg btn-block" class:btn-loading={isLoading} disabled={isLoading}>
<span class="txt">Set new password</span>
</button>
</form>
<div class="content txt-center">
<a href="/login" class="link-hint" use:link>Back to login</a>
</div>
</FullPage>
@@ -0,0 +1,66 @@
<script>
import { link } from "svelte-spa-router";
import { replace } from "svelte-spa-router";
import FullPage from "@/components/base/FullPage.svelte";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import Field from "@/components/base/Field.svelte";
import { addErrorToast } from "@/stores/toasts";
const queryParams = CommonHelper.getQueryParams(window.location?.href);
let email = queryParams.demoEmail || "";
let password = queryParams.demoPassword || "";
let isLoading = false;
function login() {
if (isLoading) {
return;
}
isLoading = true;
return ApiClient.Admins.authViaEmail(email, password)
.then(() => {
replace("/");
})
.catch(() => {
addErrorToast("Invalid login credentials.");
})
.finally(() => {
isLoading = false;
});
}
</script>
<FullPage>
<form class="block" on:submit|preventDefault={login}>
<div class="content txt-center m-b-base">
<h4>Admin sign in</h4>
</div>
<Field class="form-field required" name="email" let:uniqueId>
<label for={uniqueId}>Email</label>
<!-- svelte-ignore a11y-autofocus -->
<input type="email" id={uniqueId} bind:value={email} required autofocus />
</Field>
<Field class="form-field required" name="password" let:uniqueId>
<label for={uniqueId}>Password</label>
<input type="password" id={uniqueId} bind:value={password} required />
<div class="help-block">
<a href="/request-password-reset" class="link-hint" use:link>Forgotten password?</a>
</div>
</Field>
<button
type="submit"
class="btn btn-lg btn-block btn-next"
class:btn-disabled={isLoading}
class:btn-loading={isLoading}
>
<span class="txt">Login</span>
<i class="ri-arrow-right-line" />
</button>
</form>
</FullPage>
@@ -0,0 +1,65 @@
<script>
import { link } from "svelte-spa-router";
import ApiClient from "@/utils/ApiClient";
import FullPage from "@/components/base/FullPage.svelte";
import Field from "@/components/base/Field.svelte";
let email = "";
let isLoading = false;
let success = false;
async function submit() {
if (isLoading) {
return;
}
isLoading = true;
try {
await ApiClient.Admins.requestPasswordReset(email);
success = true;
} catch (err) {
ApiClient.errorResponseHandler(err);
}
isLoading = false;
}
</script>
<FullPage>
{#if success}
<div class="alert alert-success">
<div class="icon"><i class="ri-checkbox-circle-line" /></div>
<div class="content">
<p>Check <strong class="txt-nowrap">{email}</strong> for the recovery link.</p>
</div>
</div>
{:else}
<form class="m-b-base" on:submit|preventDefault={submit}>
<div class="content txt-center m-b-sm">
<h4 class="m-b-xs">Forgotten admin password</h4>
<p>Enter the email associated with your account and well send you a recovery link:</p>
</div>
<Field class="form-field required" name="email" let:uniqueId>
<label for={uniqueId}>Email</label>
<!-- svelte-ignore a11y-autofocus -->
<input type="email" id={uniqueId} required autofocus bind:value={email} />
</Field>
<button
type="submit"
class="btn btn-lg btn-block"
class:btn-loading={isLoading}
disabled={isLoading}
>
<i class="ri-mail-send-line" />
<span class="txt">Send recovery link</span>
</button>
</form>
{/if}
<div class="content txt-center">
<a href="/login" class="link-hint" use:link>Back to login</a>
</div>
</FullPage>
+198
View File
@@ -0,0 +1,198 @@
<script>
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { admin as loggedAdmin } from "@/stores/admin";
import Searchbar from "@/components/base/Searchbar.svelte";
import SortHeader from "@/components/base/SortHeader.svelte";
import IdLabel from "@/components/base/IdLabel.svelte";
import FormattedDate from "@/components/base/FormattedDate.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
import AdminUpsertPanel from "@/components/admins/AdminUpsertPanel.svelte";
const queryParams = CommonHelper.getQueryParams(window.location?.href);
let adminUpsertPanel;
let admins = [];
let isLoading = false;
let filter = queryParams.filter || "";
let sort = queryParams.sort || "-created";
$: if (sort !== -1 && filter !== -1) {
// keep listing params in sync
CommonHelper.replaceClientQueryParams({
filter: filter,
sort: sort,
});
loadAdmins();
}
CommonHelper.setDocumentTitle("Admins");
export function loadAdmins() {
isLoading = true;
admins = []; // reset
return ApiClient.Admins.getFullList(100, {
sort: sort || "-created",
filter: filter,
})
.then((result) => {
admins = result;
isLoading = false;
})
.catch((err) => {
if (err !== null) {
isLoading = false;
console.warn(err);
clearList();
ApiClient.errorResponseHandler(err, false);
}
});
}
function clearList() {
admins = [];
}
</script>
<SettingsSidebar />
<main class="page-wrapper">
<header class="page-header">
<nav class="breadcrumbs">
<div class="breadcrumb-item">Settings</div>
<div class="breadcrumb-item">Admins</div>
</nav>
<div class="flex-fill" />
<button type="button" class="btn btn-expanded" on:click={() => adminUpsertPanel?.show()}>
<i class="ri-add-line" />
<span class="txt">New admin</span>
</button>
</header>
<Searchbar
value={filter}
placeholder={"Search filter, eg. email='test@example.com'"}
extraAutocompleteKeys={["email"]}
on:submit={(e) => (filter = e.detail)}
/>
<div class="table-wrapper">
<table class="table" class:table-loading={isLoading}>
<thead>
<tr>
<th class="min-width" />
<SortHeader class="col-type-text" name="id" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("primary")} />
<span class="txt">id</span>
</div>
</SortHeader>
<SortHeader class="col-type-email col-field-email" name="email" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("email")} />
<span class="txt">email</span>
</div>
</SortHeader>
<SortHeader class="col-type-date col-field-created" name="created" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("date")} />
<span class="txt">created</span>
</div>
</SortHeader>
<SortHeader class="col-type-date col-field-updated" name="updated" bind:sort>
<div class="col-header-content">
<i class={CommonHelper.getFieldTypeIcon("date")} />
<span class="txt">updated</span>
</div>
</SortHeader>
<th class="col-type-action min-width" />
</tr>
</thead>
<tbody>
{#each admins as admin (admin.id)}
<tr
tabindex="0"
class="row-handle"
on:click={() => adminUpsertPanel?.show(admin)}
on:keydown={(e) => {
if (e.code === "Enter" || e.code === "Space") {
e.preventDefault();
adminUpsertPanel?.show(admin);
}
}}
>
<td class="min-width">
<figure class="thumb thumb-sm thumb-circle">
<img
src="{import.meta.env.BASE_URL}images/avatars/avatar{admin.avatar ||
0}.svg"
alt="Admin avatar"
/>
</figure>
</td>
<td class="col-type-text col-field-id">
<IdLabel id={admin.id} />
{#if admin.id === $loggedAdmin.id}
<span class="label label-warning m-l-5">You</span>
{/if}
</td>
<td class="col-type-email col-field-email">
<span class="txt txt-ellipsis" title={admin.email}>
{admin.email}
</span>
</td>
<td class="col-type-date col-field-created">
<FormattedDate date={admin.created} />
</td>
<td class="col-type-date col-field-updated">
<FormattedDate date={admin.updated} />
</td>
<td class="col-type-action min-width">
<i class="ri-arrow-right-line" />
</td>
</tr>
{:else}
{#if isLoading}
<tr>
<td colspan="99" class="p-xs">
<span class="skeleton-loader" />
</td>
</tr>
{:else}
<tr>
<td colspan="99" class="txt-center txt-hint p-xs">
<h6>No admins found.</h6>
{#if filter?.length}
<button
type="button"
class="btn btn-hint btn-expanded m-t-sm"
on:click={() => (filter = "")}
>
<span class="txt">Clear filters</span>
</button>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{#if admins.length}
<small class="block txt-hint txt-right m-t-sm">Showing {admins.length} of {admins.length}</small>
{/if}
</main>
<AdminUpsertPanel bind:this={adminUpsertPanel} on:save={() => loadAdmins()} on:delete={() => loadAdmins()} />