Lots of changes:
- Switching build system to pure rollup without too much custom logic - Restructuring files - Adding Popup View - Make everything typescript compatible - Adding @hibas123/theme - Start switching to @hibas123/theme
This commit is contained in:
192
src/pages/User/Pages/Account.svelte
Normal file
192
src/pages/User/Pages/Account.svelte
Normal file
@ -0,0 +1,192 @@
|
||||
<script>
|
||||
import Box from "./Box.svelte";
|
||||
import BoxItem from "./BoxItem.svelte";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
|
||||
import request from "../../../helper/request.ts";
|
||||
|
||||
export let loading = false;
|
||||
let account_error = undefined;
|
||||
let contact_error = undefined;
|
||||
|
||||
const genderMap = new Map();
|
||||
genderMap.set(0, "None");
|
||||
genderMap.set(1, "Male");
|
||||
genderMap.set(2, "Female");
|
||||
genderMap.set(3, "Other");
|
||||
|
||||
let name = "";
|
||||
let gender = 0;
|
||||
$: genderHuman = genderMap.get(gender) || "ERROR";
|
||||
let birthday = undefined;
|
||||
|
||||
async function saveName() {
|
||||
//TODO: implement
|
||||
await load();
|
||||
}
|
||||
|
||||
async function saveGender() {
|
||||
//TODO: implement
|
||||
await load();
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
let { user } = await request(
|
||||
"/api/user/account",
|
||||
{},
|
||||
"GET",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
name = user.name;
|
||||
// username = user.username;
|
||||
gender = user.gender;
|
||||
birthday = user.birthday
|
||||
? new Date(user.birthday).toLocaleDateString()
|
||||
: undefined;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
account_error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
let email = [];
|
||||
let phone = [];
|
||||
|
||||
async function loadContact() {
|
||||
try {
|
||||
let { contact } = await request(
|
||||
"/api/user/contact",
|
||||
{},
|
||||
"GET",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
email = contact.mails.map((e) => e.mail);
|
||||
phone = contact.phones.map((e) => e.phone);
|
||||
contact_error = undefined;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
contact_error = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
await Promise.all([loadProfile(), loadContact()]);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
background-color: var(--primary);
|
||||
margin: auto 0;
|
||||
margin-left: 1rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.floating {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-container > *:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: unset;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: unset;
|
||||
font-size: unset;
|
||||
border-bottom: 1px solid #757575;
|
||||
/* Firefox */
|
||||
-moz-appearance: none;
|
||||
/* Safari and Chrome */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select > option {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-wrapper::after {
|
||||
content: ">";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1rem;
|
||||
transform: rotate(90deg) scaleY(2);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Box>
|
||||
<h1>Profile</h1>
|
||||
{#if account_error}
|
||||
<p class="error">{account_error}</p>
|
||||
{/if}
|
||||
<BoxItem name="Name" value={name}>
|
||||
<div class="input-container">
|
||||
<div class="floating group">
|
||||
<input
|
||||
id="name-inp"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
bind:value={name} />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="name-inp">Name</label>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Gender" value={genderHuman}>
|
||||
<div class="input-container">
|
||||
<div class="select-wrapper">
|
||||
<select bind:value={gender}>
|
||||
<option value={1}>Male</option>
|
||||
<option value={2}>Female</option>
|
||||
<option value={3}>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" on:click={saveGender}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Birthday" value={birthday} />
|
||||
<BoxItem name="Password" value="******" />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<h1>Contact</h1>
|
||||
{#if contact_error}
|
||||
<p class="error">{contact_error}</p>
|
||||
{/if}
|
||||
<BoxItem name="E-Mail" value={email} noOpen={true} />
|
||||
<BoxItem name="Phone" value={phone} noOpen={true} />
|
||||
</Box>
|
36
src/pages/User/Pages/Box.svelte
Normal file
36
src/pages/User/Pages/Box.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<style>
|
||||
.box {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.30), 0 5px 4px rgba(0, 0, 0, 0.22);
|
||||
padding: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.box> :global(h1) {
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #444444;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.box> :global(div) {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.box> :global(div):first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@media (min-width: 45rem) {
|
||||
.box {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="box">
|
||||
<slot></slot>
|
||||
</div>
|
94
src/pages/User/Pages/BoxItem.svelte
Normal file
94
src/pages/User/Pages/BoxItem.svelte
Normal file
@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
export let name = "";
|
||||
export let value = "";
|
||||
export let noOpen = false;
|
||||
export let open = false;
|
||||
export let highlight = false;
|
||||
|
||||
function toggleOpen(ev) {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.root:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.values {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: calc(100% - var(--default-font-size) - 16px);
|
||||
}
|
||||
|
||||
.values > div:first-child {
|
||||
transform-origin: left;
|
||||
transform: scale(0.95);
|
||||
margin-right: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.values > div:nth-child(2) {
|
||||
color: black;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
margin: auto 8px auto 8px;
|
||||
height: var(--default-font-size);
|
||||
min-width: var(--default-font-size);
|
||||
}
|
||||
|
||||
.body {
|
||||
box-sizing: border-box;
|
||||
padding: 0.1px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 45rem) {
|
||||
.values {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.values > div:first-child {
|
||||
transform: unset;
|
||||
flex-basis: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-element {
|
||||
background-color: #7bff003b;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="root" class:highlight-element={highlight}>
|
||||
<div class="container" on:click={() => (open = !open)}>
|
||||
<div class="values">
|
||||
<div>{name}</div>
|
||||
<div>
|
||||
{#if Array.isArray(value)}
|
||||
{#each value as v, i}
|
||||
{v}
|
||||
{#if i < value.length - 1}
|
||||
<br />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}{value}{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if !noOpen}
|
||||
<NextIcon rotation={open ? -90 : 90} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if open && !noOpen}
|
||||
<div class="body" transition:slide>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
13
src/pages/User/Pages/NextIcon.svelte
Normal file
13
src/pages/User/Pages/NextIcon.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<script>
|
||||
export let rotation;
|
||||
</script>
|
||||
|
||||
<svg style={`enable-background:new 0 0 35.414 35.414; transform: rotate(${rotation}deg); transition: all .4s;`}
|
||||
version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 35.414 35.414" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="27.051,17 9.905,0 8.417,1.414 24.674,17.707 8.363,34 9.914,35.414 27.051,18.414 " />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
188
src/pages/User/Pages/Security.svelte
Normal file
188
src/pages/User/Pages/Security.svelte
Normal file
@ -0,0 +1,188 @@
|
||||
<script context="module">
|
||||
const TFATypes = new Map();
|
||||
TFATypes.set(0, "Authenticator");
|
||||
TFATypes.set(1, "Backup Codes");
|
||||
TFATypes.set(2, "YubiKey");
|
||||
TFATypes.set(3, "Push Notification");
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Box from "./Box.svelte";
|
||||
import BoxItem from "./BoxItem.svelte";
|
||||
import NextIcon from "./NextIcon.svelte";
|
||||
import request from "../../../helper/request.ts";
|
||||
|
||||
export let loading = false;
|
||||
|
||||
let twofactor = [];
|
||||
|
||||
async function deleteTFA(id) {
|
||||
let res = await request(
|
||||
"/api/user/twofactor/" + id,
|
||||
undefined,
|
||||
"DELETE",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
loadTwoFactor();
|
||||
}
|
||||
|
||||
async function loadTwoFactor() {
|
||||
let res = await request(
|
||||
"/api/user/twofactor",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
twofactor = res.methods;
|
||||
}
|
||||
|
||||
let token = [];
|
||||
|
||||
async function revoke(id) {
|
||||
let res = await request(
|
||||
"/api/user/token/" + id,
|
||||
undefined,
|
||||
"DELETE",
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
loadToken();
|
||||
}
|
||||
|
||||
async function loadToken() {
|
||||
loading = true;
|
||||
let res = await request(
|
||||
"/api/user/token",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true
|
||||
);
|
||||
token = res.token;
|
||||
loading = false;
|
||||
}
|
||||
|
||||
loadToken();
|
||||
loadTwoFactor();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
background-color: var(--primary);
|
||||
margin: auto 0;
|
||||
margin-left: 1rem;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.floating {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-container > *:first-child {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: unset;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: unset;
|
||||
font-size: unset;
|
||||
border-bottom: 1px solid #757575;
|
||||
/* Firefox */
|
||||
-moz-appearance: none;
|
||||
/* Safari and Chrome */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select > option {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-wrapper::after {
|
||||
content: ">";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 2rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1rem;
|
||||
transform: rotate(90deg) scaleY(2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<Box>
|
||||
<h1>Two Factor</h1>
|
||||
<BoxItem name="Add new" open={false} />
|
||||
{#each twofactor as t}
|
||||
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
|
||||
<button
|
||||
class="btn"
|
||||
style="background: var(--error)"
|
||||
on:click={() => deleteTFA(t.id)}>
|
||||
Delete
|
||||
</button>
|
||||
</BoxItem>
|
||||
{/each}
|
||||
<!-- <BoxItem name="Name" value={name} open={false}>
|
||||
<div class="input-container">
|
||||
<div class="floating group">
|
||||
<input type="text" autocomplete="username" bind:value={name}>
|
||||
<span class="highlight"></span>
|
||||
<span class="bar"></span>
|
||||
<label>Name</label>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Gender" value={gender} open={true}>
|
||||
<div class="input-container">
|
||||
<div class="select-wrapper">
|
||||
<select>
|
||||
<option value="1">Male</option>
|
||||
<option value="2">Female</option>
|
||||
<option value="3">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" on:click={saveName}>Save</button>
|
||||
</div>
|
||||
</BoxItem>
|
||||
<BoxItem name="Birthday" value={birthday} />
|
||||
<BoxItem name="Password" value="******" /> -->
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<h1>Anmeldungen</h1>
|
||||
|
||||
{#each token as t}
|
||||
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
|
||||
<button
|
||||
class="btn"
|
||||
style="background: var(--error)"
|
||||
on:click={() => revoke(t.id)}>
|
||||
Revoke
|
||||
</button>
|
||||
</BoxItem>
|
||||
{:else}<span>No Tokens</span>{/each}
|
||||
|
||||
<!-- <BoxItem name="E-Mail" value={email} />
|
||||
<BoxItem name="Phone" value={phone} /> -->
|
||||
</Box>
|
Reference in New Issue
Block a user