Start of many new pages
This commit is contained in:
parent
95fb591b4b
commit
76c2680c60
@ -4,7 +4,8 @@
|
||||
|------------------|------------------------|
|
||||
| `00000001` | View postings |
|
||||
| `00000010` | View account |
|
||||
| `00000100` | Submit postings access |
|
||||
| `00001000` | Manage postings |
|
||||
| `00010000` | Manage users |
|
||||
| `00100000` | Apply |
|
||||
| `00000100` | Apply for jobs |
|
||||
| `00001000` | Submit postings |
|
||||
| `00010000` | Manage tags |
|
||||
| `00100000` | Manage postings |
|
||||
| `01000000` | Manage users |
|
||||
|
||||
89
src/app.css
89
src/app.css
@ -2,22 +2,29 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
|
||||
[data-theme='light'] {
|
||||
--text-color: #000000;
|
||||
--bg-color: #f4f4f4;
|
||||
--hover-bg-color: #e4e4f0;
|
||||
--elevated-bg-color: #e0e0e0;
|
||||
--separator-line-color: #e0e0e0;
|
||||
--low-emphasis-text-color: #6b6b6b;
|
||||
--primary-color: #1F96F3;
|
||||
--dull-primary-color: #51aaf0;
|
||||
--elevated-bg-color: #ffffff;
|
||||
--bg-accent-color: #f4f4f4;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--text-color: #f4f4f4;
|
||||
--bg-color: #080808;
|
||||
--hover-bg-color: #1f2937;
|
||||
--elevated-bg-color: #1a202c;
|
||||
--separator-line-color: #1a2029;
|
||||
--low-emphasis-text-color: #999999;
|
||||
--primary-color: #1F96F3;
|
||||
--dull-primary-color: #1569ab;
|
||||
--elevated-bg-color: #0c0c0d;
|
||||
--bg-accent-color: #202020;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -31,7 +38,7 @@ h1 {
|
||||
}
|
||||
|
||||
.elevated {
|
||||
background-color: var(--elevated-bg-color);
|
||||
background-color: var(--separator-line-color);
|
||||
@apply shadow-lg
|
||||
}
|
||||
|
||||
@ -52,11 +59,11 @@ h1 {
|
||||
}
|
||||
|
||||
.top-border {
|
||||
border-top: 1px solid var(--elevated-bg-color);
|
||||
border-top: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.bottom-border {
|
||||
border-bottom: 1px solid var(--elevated-bg-color);
|
||||
border-bottom: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.icon-16 {
|
||||
@ -77,18 +84,27 @@ h1 {
|
||||
'opsz' 20
|
||||
}
|
||||
|
||||
input[type='search'] {
|
||||
input[type='search'], input[type='text'], input[type='password'] {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--elevated-bg-color);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
input[type='search']:focus {
|
||||
input[type='search']:focus, input[type='text']:focus, input[type='password']:focus{
|
||||
outline: 0 solid var(--text-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 0 0 var(--primary-color);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
border: 1px solid var(--separator-line-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.arrange-vertically > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@ -97,7 +113,7 @@ input[type='search']:focus {
|
||||
|
||||
.search-bar input[type='search'] {
|
||||
flex-grow: 1;
|
||||
border: 1px solid var(--elevated-bg-color);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
outline: none;
|
||||
@ -107,9 +123,9 @@ input[type='search']:focus {
|
||||
.search-bar button {
|
||||
height: 36px;
|
||||
background: none;
|
||||
border-top: 1px solid var(--elevated-bg-color);
|
||||
border-right: 1px solid var(--elevated-bg-color);
|
||||
border-bottom: 1px solid var(--elevated-bg-color);
|
||||
border-top: 1px solid var(--separator-line-color);
|
||||
border-right: 1px solid var(--separator-line-color);
|
||||
border-bottom: 1px solid var(--separator-line-color);
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
cursor: pointer;
|
||||
@ -123,15 +139,62 @@ input[type='search']:focus {
|
||||
|
||||
}
|
||||
|
||||
.container {
|
||||
.base-container {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.signin-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
|
||||
@apply px-6;
|
||||
}
|
||||
|
||||
.search-cancel::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
background-color: var(--text-color);
|
||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23777'><path d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
|
||||
background-size: 20px 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
table, .table {
|
||||
/*border-collapse: separate;*/
|
||||
width: 100%;
|
||||
/*text-align: start;*/
|
||||
}
|
||||
|
||||
tr {
|
||||
border-top: 1px solid var(--separator-line-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th.left, td.left {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.borders {
|
||||
border: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.elevated {
|
||||
background-color: var(--elevated-bg-color);
|
||||
@apply shadow-lg
|
||||
}
|
||||
|
||||
.primary-bg-color {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.dull-primary-bg-color {
|
||||
background-color: var(--dull-primary-color);
|
||||
}
|
||||
|
||||
|
||||
9
src/lib/consts.ts
Normal file
9
src/lib/consts.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const PERMISSIONS = {
|
||||
VIEW_POSTINGS: 0b00000001,
|
||||
VIEW_ACCOUNT: 0b00000010,
|
||||
APPLY_FOR_JOBS: 0b00000100,
|
||||
SUBMIT_POSTINGS: 0b00001000,
|
||||
MANAGE_TAGS: 0b00010000,
|
||||
MANAGE_POSTINGS: 0b00100000,
|
||||
MANAGE_USERS: 0b01000000
|
||||
};
|
||||
@ -1,12 +1,17 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function createUser(username: string, password: string): Promise<void> {
|
||||
const password_hash: string = await bcrypt.hash(password, 12);
|
||||
const timestamp = new Date(Date.now()).toISOString();
|
||||
|
||||
console.log(timestamp);
|
||||
|
||||
const response = await sql`
|
||||
INSERT INTO users (username, password_hash, perms)
|
||||
VALUES (${username}, ${password_hash}, 3);
|
||||
INSERT INTO users (username, password_hash, perms, created_at, last_signin, active)
|
||||
VALUES (${username}, ${password_hash}, 3, ${timestamp}, ${timestamp}, ${true});
|
||||
`;
|
||||
}
|
||||
|
||||
@ -25,3 +30,71 @@ export async function checkUserCreds(username: string, password: string): Promis
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function getUserPerms(cookies: Cookies): number {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const JWT = cookies.get('jwt');
|
||||
if (JWT) {
|
||||
try {
|
||||
const decoded = jwt.verify(JWT, process.env.JWT_SECRET);
|
||||
if (typeof decoded === 'object' && 'perms' in decoded) {
|
||||
return decoded['perms'];
|
||||
}
|
||||
} catch (err) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
const users = await sql<
|
||||
{
|
||||
id: number;
|
||||
username: string;
|
||||
perms: number;
|
||||
created_at: Date;
|
||||
last_signin: Date;
|
||||
active: boolean;
|
||||
}[]
|
||||
>`
|
||||
SELECT id, username, perms,
|
||||
created_at AT TIME ZONE 'UTC' AS created_at,
|
||||
last_signin AT TIME ZONE 'UTC' AS last_signin,
|
||||
active
|
||||
FROM users;
|
||||
`;
|
||||
return users.map(
|
||||
(user): User => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
perms: user.perms,
|
||||
created_at: user.created_at,
|
||||
last_signin: user.last_signin,
|
||||
active: user.active
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// should require MANAGE_TAGS permission
|
||||
export async function getTags(): Promise<Tag[]> {
|
||||
const tags = await sql<
|
||||
{
|
||||
id: number;
|
||||
display_name: string;
|
||||
}[]
|
||||
>`
|
||||
SELECT id, display_name
|
||||
FROM tags;
|
||||
`;
|
||||
return tags.map(
|
||||
(tag): Tag => ({
|
||||
id: tag.id,
|
||||
display_name: tag.display_name
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
13
src/lib/types.ts
Normal file
13
src/lib/types.ts
Normal file
@ -0,0 +1,13 @@
|
||||
interface User {
|
||||
id: number | null;
|
||||
username: string;
|
||||
perms: number;
|
||||
created_at: Date | null;
|
||||
last_signin: Date | null;
|
||||
active: boolean | null;
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
id: number;
|
||||
display_name: string;
|
||||
}
|
||||
29
src/routes/+error.svelte
Normal file
29
src/routes/+error.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div style="padding-top: 32px" class="text-center">
|
||||
<h1 class="text-9xl font-bold">
|
||||
{$page.status}
|
||||
</h1>
|
||||
<h1>Thats an error</h1>
|
||||
{#if $page.status === 404}
|
||||
<p>We cant seem to find the page you are looking for.</p>
|
||||
<p>The address may be mistyped, or the page may have moved or been deleted.</p>
|
||||
{/if}
|
||||
{#if $page.status === 403}
|
||||
<p>You dont have access to this page!</p>
|
||||
<p>Please contact your admin if you think this is a mistake</p>
|
||||
{/if}
|
||||
{#if $page.status === 401}
|
||||
<p>You must be signed-in to view this page!</p>
|
||||
{/if}
|
||||
{#if $page.status === 500}
|
||||
<p>This one is on our end...</p>
|
||||
<p>We are working to resolve this as fast as possible.</p>
|
||||
{/if}
|
||||
{#if $page.status !== 404 && $page.status !== 403 && $page.status !== 401 && $page.status !== 500}
|
||||
<p>An unexpected error has occurred.</p>
|
||||
<p>Please try again later</p>
|
||||
{/if}
|
||||
</div>
|
||||
@ -2,7 +2,7 @@
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
// import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
let currentTheme: string = $state('');
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,dark_mode,group,light_mode,login,search,sell,work"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,check,close,dark_mode,edit,group,info,light_mode,login,person,search,sell,visibility,visibility_off,work"
|
||||
/>
|
||||
|
||||
<div class="bottom-border flex h-14 justify-between p-3 align-middle">
|
||||
@ -57,34 +57,28 @@
|
||||
/>
|
||||
</a>
|
||||
<a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>
|
||||
{#if (userState.perms & 0b00000001) !== 0}
|
||||
{#if (userState.perms & PERMISSIONS.VIEW_POSTINGS) !== 0}
|
||||
<a href="/listings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Listings</a>
|
||||
{/if}
|
||||
{#if (userState.perms & 0b00001000) !== 0}
|
||||
<a href="/administration/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
<a href="/admin/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
||||
>Administration</a
|
||||
>
|
||||
{/if}
|
||||
</nav>
|
||||
<div>
|
||||
<button onclick={toggleTheme} class="">
|
||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1 dark:invisible">
|
||||
{'light_mode'}
|
||||
</span>
|
||||
</button>
|
||||
<button onclick={toggleTheme} class="">
|
||||
<span
|
||||
class="material-symbols-outlined hover-bg-color invisible rounded-full p-1 dark:visible"
|
||||
>
|
||||
{'dark_mode'}
|
||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||
{currentTheme === 'light' ? 'light_mode' : 'dark_mode'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onclick={() =>
|
||||
(window.location.href = (userState.perms & 0b00000010) !== 0 ? '/account' : '/signin')}
|
||||
(window.location.href =
|
||||
(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? '/account' : '/signin')}
|
||||
>
|
||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||
{(userState.perms & 0b00000010) !== 0 ? 'account_circle' : 'login'}
|
||||
{(userState.perms & PERMISSIONS.VIEW_ACCOUNT) !== 0 ? 'account_circle' : 'login'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
9
src/routes/account/+page.svelte
Normal file
9
src/routes/account/+page.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (!document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -7,28 +7,27 @@
|
||||
|
||||
<div class="bottom-border h-10 pt-2 text-center">
|
||||
<a
|
||||
href="/administration/postings"
|
||||
class="p-2 {$page.url.pathname === '/administration/postings'
|
||||
href="/admin/postings"
|
||||
class="p-2 {$page.url.pathname === '/admin/postings'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'}"
|
||||
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
||||
>
|
||||
<a
|
||||
href="/administration/users"
|
||||
class="p-2 {$page.url.pathname === '/administration/users'
|
||||
href="/admin/users"
|
||||
class="p-2 {$page.url.pathname === '/admin/users'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'}"
|
||||
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
||||
>
|
||||
<a
|
||||
href="/administration/tags"
|
||||
class="{$page.url.pathname === '/administration/tags'
|
||||
><a
|
||||
href="/admin/tags"
|
||||
class="{$page.url.pathname === '/admin/tags'
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text'} p-2"
|
||||
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="base-container">
|
||||
{@render children()}
|
||||
</div>
|
||||
7
src/routes/admin/+page.svelte
Normal file
7
src/routes/admin/+page.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
window.location.href = '/admin/postings';
|
||||
});
|
||||
</script>
|
||||
14
src/routes/admin/tags/+page.server.ts
Normal file
14
src/routes/admin/tags/+page.server.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms, getTags } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_TAGS) > 0) {
|
||||
return {
|
||||
tags: await getTags()
|
||||
};
|
||||
}
|
||||
error(401, 'Unauthorized');
|
||||
};
|
||||
69
src/routes/admin/tags/+page.svelte
Normal file
69
src/routes/admin/tags/+page.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">
|
||||
Tag Management (Total: {data.tags?.length || 0})
|
||||
</div>
|
||||
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/admin/tags/create"
|
||||
>Create New Tag</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
<div class="flex py-4">
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="search"
|
||||
name="searchTags"
|
||||
id="searchTags"
|
||||
placeholder="Search Tags"
|
||||
class="search-cancel"
|
||||
/>
|
||||
<button><span class="material-symbols-outlined">search</span></button>
|
||||
</div>
|
||||
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left py-1">ID</th>
|
||||
<th class="py-1">Name</th>
|
||||
<th class="py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.tags !== undefined}
|
||||
{#each data.tags as tag}
|
||||
<tr>
|
||||
<td class="left">{tag.id}</td>
|
||||
<td>{tag.display_name}</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||
href="/admin/tags/{tag.id}">info</a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/admin/tags/{tag.id}/edit">edit</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
14
src/routes/admin/users/+page.server.ts
Normal file
14
src/routes/admin/users/+page.server.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms, getUsers } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
return {
|
||||
users: await getUsers()
|
||||
};
|
||||
}
|
||||
error(401, 'Unauthorized');
|
||||
};
|
||||
92
src/routes/admin/users/+page.svelte
Normal file
92
src/routes/admin/users/+page.svelte
Normal file
@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
// console.log(data);
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">
|
||||
User Account Management (Total: {data.users?.length || 0})
|
||||
</div>
|
||||
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/admin/users/create"
|
||||
>Create New User</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
<div class="flex py-4">
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="search"
|
||||
name="searchUsers"
|
||||
id="searchUsers"
|
||||
placeholder="Search Users"
|
||||
class="search-cancel"
|
||||
/>
|
||||
<button><span class="material-symbols-outlined">search</span></button>
|
||||
</div>
|
||||
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left py-1">ID</th>
|
||||
<th class="py-1">Username</th>
|
||||
<th class="py-1">Permissions</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="py-1">Last Sign-In</th>
|
||||
<th class="py-1">Active</th>
|
||||
<th class="py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.users !== undefined}
|
||||
{#each data.users as user}
|
||||
<tr>
|
||||
<td class="left">{user.id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.perms}</td>
|
||||
<td
|
||||
>{user.created_at?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td
|
||||
>{user.last_signin?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="material-symbols-outlined py-2">
|
||||
{user.active ? 'check' : 'close'}
|
||||
</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||
href="/admin/users/{user.id}">person</a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/admin/users/{user.id}/edit">edit</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,18 +0,0 @@
|
||||
<div class="content">
|
||||
<form action="">
|
||||
<div class="flex py-4">
|
||||
<div class="search-bar">
|
||||
<input type="search" name="searchUsers" id="searchUsers" placeholder="Search Users" />
|
||||
<button><span class="material-symbols-outlined">search</span></button>
|
||||
</div>
|
||||
<button class="hover-bg-color mx-2 mr-1 rounded px-3 py-2 text-sm"
|
||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
<button class="hover-bg-color mr-1 rounded px-3 py-2 text-sm"
|
||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
0
src/routes/register/employer/+page.svelte
Normal file
0
src/routes/register/employer/+page.svelte
Normal file
0
src/routes/register/user/+page.svelte
Normal file
0
src/routes/register/user/+page.svelte
Normal file
@ -38,7 +38,7 @@ export const actions: Actions = {
|
||||
}
|
||||
},
|
||||
|
||||
login: async ({ request, cookies }) => {
|
||||
signin: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString();
|
||||
const password = data.get('password')?.toString();
|
||||
|
||||
@ -1,22 +1,88 @@
|
||||
<script lang="ts">
|
||||
import type { ActionData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
|
||||
let passwordVisible = $state(false);
|
||||
|
||||
function showPassword() {
|
||||
const password = document.querySelector('input[name="password"]');
|
||||
if (password) {
|
||||
if (password.getAttribute('type') === 'password') {
|
||||
password.setAttribute('type', 'text');
|
||||
passwordVisible = true;
|
||||
} else {
|
||||
password.setAttribute('type', 'password');
|
||||
passwordVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// receive form data from server
|
||||
let form: ActionData;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1 class="is-size-3 has-text-weight-semibold my-4">Sign In or Register</h1>
|
||||
<form method="POST">
|
||||
<input class="input my-2" type="text" placeholder="Username" name="username" required />
|
||||
<input class="input my-2" type="password" placeholder="Password" name="password" required />
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated content rounded-md p-8">
|
||||
<h1 class="is-size-3 has-text-weight-semibold my-4">Welcome Back!</h1>
|
||||
<form method="POST" class="arrange-vertically">
|
||||
<input
|
||||
class="input-field my-2 w-full"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
class="input-field my-2 w-full pr-10"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={showPassword}
|
||||
class="absolute right-2.5 top-8 -translate-y-1/2 transform"
|
||||
>
|
||||
<span class="material-symbols-outlined"
|
||||
>{passwordVisible ? 'visibility' : 'visibility_off'}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- display error message -->
|
||||
<!-- TODO: fix -->
|
||||
{#if form?.errorMessage}
|
||||
<div class="has-text-danger my-2">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button class="button mr-3 mt-4" type="submit" formaction="?/register">Register</button>
|
||||
<button class="button is-primary mt-4" type="submit" formaction="?/login">Sign In</button>
|
||||
<button
|
||||
class="primary-bg-color mt-6 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/signin">Sign In</button
|
||||
>
|
||||
<a href="/register" class="low-emphasis-text mt-2">Don't have an account? Register here.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<div class="container">-->
|
||||
<!-- <h1 class="is-size-3 has-text-weight-semibold my-4">Sign In or Register</h1>-->
|
||||
<!-- <form method="POST">-->
|
||||
<!-- <input class="input my-2" type="text" placeholder="Username" name="username" required />-->
|
||||
<!-- <input class="input my-2" type="password" placeholder="Password" name="password" required />-->
|
||||
|
||||
<!-- <!– display error message –>-->
|
||||
<!-- {#if form?.errorMessage}-->
|
||||
<!-- <div class="has-text-danger my-2">{form.errorMessage}</div>-->
|
||||
<!-- {/if}-->
|
||||
|
||||
<!-- <button class="button mr-3 mt-4" type="submit" formaction="?/register">Register</button>-->
|
||||
<!-- <button class="button is-primary mt-4" type="submit" formaction="?/login">Sign In</button>-->
|
||||
<!-- </form>-->
|
||||
<!--</div>-->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user