forked from mirrors/homebox
feat: user profiles (#32)
* add user profiles and theme selectors * lowercase buttons by default * basic layout * (wip) init token APIs * refactor server to support variable options * fix types * api refactor / registration tests * implement UI for url and join * remove console.logs * rename repository factory * fix upload size
This commit is contained in:
parent
1ca430af21
commit
79f7ad40cb
76 changed files with 5154 additions and 388 deletions
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<NuxtLayout>
|
||||
<Html lang="en" data-theme="garden" />
|
||||
<Html lang="en" :data-theme="theme" />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { theme } = useTheme();
|
||||
</script>
|
||||
|
|
|
@ -29,8 +29,9 @@
|
|||
/>
|
||||
<path d="M5443.74 520.879v4149.79" style="fill: none; stroke: #000; stroke-width: 153.5px" />
|
||||
<path
|
||||
class="bg-primary"
|
||||
d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59Z"
|
||||
style="fill: #567f67"
|
||||
style="fill: hsl(var(--p) / var(--tw-bg-opacity))"
|
||||
/>
|
||||
<path
|
||||
d="M8951.41 4102.72c0-41.65-22.221-80.136-58.291-100.961-36.069-20.825-80.51-20.825-116.58 0l-2439.92 1408.69c-36.07 20.825-58.29 59.311-58.29 100.961V7058c0 41.65 22.22 80.136 58.29 100.961 36.07 20.825 80.51 20.825 116.58 0l2439.92-1408.69c36.07-20.825 58.291-59.312 58.291-100.962v-1546.59ZM6463.98 5551.29v1387.06l2301.77-1328.92V4222.37L6463.98 5551.29Z"
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
'btn-sm': size === 'sm',
|
||||
'btn-lg': size === 'lg',
|
||||
}"
|
||||
:style="upper ? '' : 'text-transform: none'"
|
||||
>
|
||||
<label v-if="$slots.icon" class="swap swap-rotate mr-2" :class="{ 'swap-active': isHover }">
|
||||
<slot name="icon" />
|
||||
|
@ -38,6 +39,10 @@
|
|||
type Sizes = "sm" | "md" | "lg";
|
||||
|
||||
const props = defineProps({
|
||||
upper: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<template>
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
<slot name="subtitle"></slot>
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-t border-gray-300 px-4 py-5 sm:p-0">
|
||||
<dl class="sm:divide-y sm:divide-gray-300">
|
||||
<div v-for="(dValue, dKey) in details" :key="dKey" class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
{{ dKey }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<slot :name="rmSpace(dKey)" v-bind="{ key: dKey, value: dValue }">
|
||||
{{ dValue }}
|
||||
</slot>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type StringLike = string | number | boolean;
|
||||
|
||||
function rmSpace(str: string) {
|
||||
return str.replace(" ", "");
|
||||
}
|
||||
|
||||
defineProps({
|
||||
details: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Object as () => Record<string, StringLike | any>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -9,7 +9,7 @@
|
|||
>
|
||||
<slot />
|
||||
</h3>
|
||||
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-base-content">
|
||||
<slot name="description" />
|
||||
</p>
|
||||
<div v-if="$slots.after">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="label" class="dropdown dropdown-end w-full">
|
||||
<div ref="label" class="dropdown dropdown-end dropdown-top w-full">
|
||||
<FormTextField v-model="dateText" tabindex="0" label="Date" :inline="inline" readonly />
|
||||
<div tabindex="0" class="mt-1 card compact dropdown-content shadow bg-base-100 rounded-box w-64" @blur="resetTime">
|
||||
<div class="card-body">
|
||||
|
|
37
frontend/components/global/CopyText.vue
Normal file
37
frontend/components/global/CopyText.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<button class="btn btn-outline btn-square btn-sm" @click="copyText">
|
||||
<label
|
||||
class="swap swap-rotate"
|
||||
:class="{
|
||||
'swap-active': copied,
|
||||
}"
|
||||
>
|
||||
<Icon class="swap-off h-5 w-5" name="mdi-content-copy" />
|
||||
<Icon class="swap-on h-5 w-5" name="mdi-clipboard" />
|
||||
</label>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String as () => string,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const copied = ref(false);
|
||||
|
||||
const { copy } = useClipboard();
|
||||
|
||||
function copyText() {
|
||||
copy(props.text);
|
||||
copied.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -2,10 +2,10 @@
|
|||
<div class="border-t border-gray-300 px-4 py-5 sm:p-0">
|
||||
<dl class="sm:divide-y sm:divide-gray-300">
|
||||
<div v-for="(detail, i) in details" :key="i" class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<dt class="text-sm font-medium text-base-content">
|
||||
{{ detail.name }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<dd class="mt-1 text-sm text-base-content sm:col-span-2 sm:mt-0">
|
||||
<slot :name="detail.slot || detail.name" v-bind="{ detail }">
|
||||
<template v-if="detail.type == 'date'">
|
||||
<DateTime :date="detail.text" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PublicApi } from "~~/lib/api/public";
|
||||
import { UserApi } from "~~/lib/api/user";
|
||||
import { UserClient } from "~~/lib/api/user";
|
||||
import { Requests } from "~~/lib/requests";
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
|
||||
|
@ -28,7 +28,7 @@ export function usePublicApi(): PublicApi {
|
|||
return new PublicApi(requests);
|
||||
}
|
||||
|
||||
export function useUserApi(): UserApi {
|
||||
export function useUserApi(): UserClient {
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const requests = new Requests("", () => authStore.token, {});
|
||||
|
@ -43,5 +43,5 @@ export function useUserApi(): UserApi {
|
|||
requests.addResponseInterceptor(observer.handler);
|
||||
}
|
||||
|
||||
return new UserApi(requests);
|
||||
return new UserClient(requests);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,41 @@
|
|||
import { Ref } from "vue";
|
||||
|
||||
export type DaisyTheme =
|
||||
| "light"
|
||||
| "dark"
|
||||
| "cupcake"
|
||||
| "bumblebee"
|
||||
| "emerald"
|
||||
| "corporate"
|
||||
| "synthwave"
|
||||
| "retro"
|
||||
| "cyberpunk"
|
||||
| "valentine"
|
||||
| "halloween"
|
||||
| "garden"
|
||||
| "forest"
|
||||
| "aqua"
|
||||
| "lofi"
|
||||
| "pastel"
|
||||
| "fantasy"
|
||||
| "wireframe"
|
||||
| "black"
|
||||
| "luxury"
|
||||
| "dracula"
|
||||
| "cmyk"
|
||||
| "autumn"
|
||||
| "business"
|
||||
| "acid"
|
||||
| "lemonade"
|
||||
| "night"
|
||||
| "coffee"
|
||||
| "winter";
|
||||
|
||||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
showEmpty: boolean;
|
||||
editorSimpleView: boolean;
|
||||
theme: DaisyTheme;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -17,6 +49,7 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
|||
showDetails: true,
|
||||
showEmpty: true,
|
||||
editorSimpleView: true,
|
||||
theme: "garden",
|
||||
},
|
||||
{ mergeDefaults: true }
|
||||
);
|
||||
|
|
40
frontend/composables/use-theme.ts
Normal file
40
frontend/composables/use-theme.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ComputedRef } from "vue";
|
||||
import { DaisyTheme } from "./use-preferences";
|
||||
|
||||
export interface UseTheme {
|
||||
theme: ComputedRef<DaisyTheme>;
|
||||
setTheme: (theme: DaisyTheme) => void;
|
||||
}
|
||||
|
||||
const themeRef = ref<DaisyTheme>("garden");
|
||||
|
||||
export function useTheme(): UseTheme {
|
||||
const preferences = useViewPreferences();
|
||||
themeRef.value = preferences.value.theme;
|
||||
|
||||
const setTheme = (newTheme: DaisyTheme) => {
|
||||
preferences.value.theme = newTheme;
|
||||
|
||||
if (htmlEl) {
|
||||
htmlEl.value.setAttribute("data-theme", newTheme);
|
||||
}
|
||||
|
||||
themeRef.value = newTheme;
|
||||
};
|
||||
|
||||
const htmlEl = ref<HTMLElement>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (htmlEl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
htmlEl.value = document.querySelector("html");
|
||||
});
|
||||
|
||||
const theme = computed(() => {
|
||||
return themeRef.value;
|
||||
});
|
||||
|
||||
return { theme, setTheme };
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { client, userClient } from "./test-utils";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { UserRegistration } from "../types/data-contracts";
|
||||
import { client, sharedUserClient, userClient } from "./test-utils";
|
||||
|
||||
function userFactory(): UserRegistration {
|
||||
return {
|
||||
email: faker.internet.email(),
|
||||
password: faker.internet.password(),
|
||||
name: faker.name.firstName(),
|
||||
groupName: faker.animal.cat(),
|
||||
token: "",
|
||||
};
|
||||
}
|
||||
|
||||
describe("[GET] /api/v1/status", () => {
|
||||
test("server should respond", async () => {
|
||||
|
@ -10,14 +22,9 @@ describe("[GET] /api/v1/status", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("first time user workflow (register, login)", () => {
|
||||
describe("first time user workflow (register, login, join group)", () => {
|
||||
const api = client();
|
||||
const userData = {
|
||||
groupName: "test-group",
|
||||
email: "test-user@email.com",
|
||||
name: "test-user",
|
||||
password: "test-password",
|
||||
};
|
||||
const userData = userFactory();
|
||||
|
||||
test("user should be able to register", async () => {
|
||||
const { response } = await api.register(userData);
|
||||
|
@ -32,8 +39,47 @@ describe("first time user workflow (register, login)", () => {
|
|||
// Cleanup
|
||||
const userApi = userClient(data.token);
|
||||
{
|
||||
const { response } = await userApi.deleteAccount();
|
||||
const { response } = await userApi.user.delete();
|
||||
expect(response.status).toBe(204);
|
||||
}
|
||||
});
|
||||
|
||||
test("user should be able to join create join token and have user signup", async () => {
|
||||
// Setup User 1 Token
|
||||
|
||||
const client = await sharedUserClient();
|
||||
const { data: user1 } = await client.user.self();
|
||||
|
||||
const { response, data } = await client.group.createInvitation({
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
uses: 1,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.token).toBeTruthy();
|
||||
|
||||
// Create User 2 with token
|
||||
|
||||
const duplicateUser = userFactory();
|
||||
duplicateUser.token = data.token;
|
||||
|
||||
const { response: registerResp } = await api.register(duplicateUser);
|
||||
expect(registerResp.status).toBe(204);
|
||||
|
||||
const { response: loginResp, data: loginData } = await api.login(duplicateUser.email, duplicateUser.password);
|
||||
expect(loginResp.status).toBe(200);
|
||||
|
||||
// Get Self and Assert
|
||||
|
||||
const client2 = userClient(loginData.token);
|
||||
|
||||
const { data: user2 } = await client2.user.self();
|
||||
|
||||
user2.item.groupName = user1.item.groupName;
|
||||
|
||||
// Cleanup User 2
|
||||
|
||||
const { response: deleteResp } = await client2.user.delete();
|
||||
expect(deleteResp.status).toBe(204);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Requests } from "../../requests";
|
|||
import { overrideParts } from "../base/urls";
|
||||
import { PublicApi } from "../public";
|
||||
import * as config from "../../../test/config";
|
||||
import { UserApi } from "../user";
|
||||
import { UserClient } from "../user";
|
||||
|
||||
export function client() {
|
||||
overrideParts(config.BASE_URL, "/api/v1");
|
||||
|
@ -14,7 +14,7 @@ export function client() {
|
|||
export function userClient(token: string) {
|
||||
overrideParts(config.BASE_URL, "/api/v1");
|
||||
const requests = new Requests("", token);
|
||||
return new UserApi(requests);
|
||||
return new UserClient(requests);
|
||||
}
|
||||
|
||||
const cache = {
|
||||
|
@ -25,7 +25,7 @@ const cache = {
|
|||
* Shared UserApi token for tests where the creation of a user is _not_ import
|
||||
* to the test. This is useful for tests that are testing the user API itself.
|
||||
*/
|
||||
export async function sharedUserClient(): Promise<UserApi> {
|
||||
export async function sharedUserClient(): Promise<UserClient> {
|
||||
if (cache.token) {
|
||||
return userClient(cache.token);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { LocationOut } from "../../types/data-contracts";
|
||||
import { AttachmentTypes } from "../../types/non-generated";
|
||||
import { UserApi } from "../../user";
|
||||
import { UserClient } from "../../user";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("user should be able to create an item and add an attachment", () => {
|
||||
|
@ -10,7 +10,7 @@ describe("user should be able to create an item and add an attachment", () => {
|
|||
* useLocatio sets up a location resource for testing, and returns a function
|
||||
* that can be used to delete the location from the backend server.
|
||||
*/
|
||||
async function useLocation(api: UserApi): Promise<[LocationOut, () => Promise<void>]> {
|
||||
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> {
|
||||
const { response, data } = await api.locations.create({
|
||||
name: `__test__.location.name_${increment}`,
|
||||
description: `__test__.location.description_${increment}`,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { LabelOut } from "../../types/data-contracts";
|
||||
import { UserApi } from "../../user";
|
||||
import { UserClient } from "../../user";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("locations lifecycle (create, update, delete)", () => {
|
||||
|
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
|
|||
* useLabel sets up a label resource for testing, and returns a function
|
||||
* that can be used to delete the label from the backend server.
|
||||
*/
|
||||
async function useLabel(api: UserApi): Promise<[LabelOut, () => Promise<void>]> {
|
||||
async function useLabel(api: UserClient): Promise<[LabelOut, () => Promise<void>]> {
|
||||
const { response, data } = await api.labels.create({
|
||||
name: `__test__.label.name_${increment}`,
|
||||
description: `__test__.label.description_${increment}`,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { LocationOut } from "../../types/data-contracts";
|
||||
import { UserApi } from "../../user";
|
||||
import { UserClient } from "../../user";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("locations lifecycle (create, update, delete)", () => {
|
||||
|
@ -10,7 +10,7 @@ describe("locations lifecycle (create, update, delete)", () => {
|
|||
* useLocatio sets up a location resource for testing, and returns a function
|
||||
* that can be used to delete the location from the backend server.
|
||||
*/
|
||||
async function useLocation(api: UserApi): Promise<[LocationOut, () => Promise<void>]> {
|
||||
async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise<void>]> {
|
||||
const { response, data } = await api.locations.create({
|
||||
name: `__test__.location.name_${increment}`,
|
||||
description: `__test__.location.description_${increment}`,
|
||||
|
|
11
frontend/lib/api/classes/group.ts
Normal file
11
frontend/lib/api/classes/group.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { GroupInvitation, GroupInvitationCreate } from "../types/data-contracts";
|
||||
|
||||
export class GroupApi extends BaseAPI {
|
||||
createInvitation(data: GroupInvitationCreate) {
|
||||
return this.http.post<GroupInvitationCreate, GroupInvitation>({
|
||||
url: route("/groups/invitations"),
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
}
|
17
frontend/lib/api/classes/users.ts
Normal file
17
frontend/lib/api/classes/users.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { UserOut } from "../types/data-contracts";
|
||||
import { Result } from "../types/non-generated";
|
||||
|
||||
export class UserApi extends BaseAPI {
|
||||
public self() {
|
||||
return this.http.get<Result<UserOut>>({ url: route("/users/self") });
|
||||
}
|
||||
|
||||
public logout() {
|
||||
return this.http.post<object, void>({ url: route("/users/logout") });
|
||||
}
|
||||
|
||||
public delete() {
|
||||
return this.http.delete<void>({ url: route("/users/self") });
|
||||
}
|
||||
}
|
|
@ -222,6 +222,7 @@ export interface UserRegistration {
|
|||
groupName: string;
|
||||
name: string;
|
||||
password: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface ApiSummary {
|
||||
|
@ -238,11 +239,22 @@ export interface Build {
|
|||
version: string;
|
||||
}
|
||||
|
||||
export interface GroupInvitation {
|
||||
expiresAt: Date;
|
||||
token: string;
|
||||
uses: number;
|
||||
}
|
||||
|
||||
export interface GroupInvitationCreate {
|
||||
expiresAt: Date;
|
||||
uses: number;
|
||||
}
|
||||
|
||||
export interface ItemAttachmentToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
expiresAt: string;
|
||||
expiresAt: Date;
|
||||
token: string;
|
||||
}
|
||||
|
|
|
@ -4,3 +4,7 @@ export enum AttachmentTypes {
|
|||
Warranty = "warranty",
|
||||
Attachment = "attachment",
|
||||
}
|
||||
|
||||
export type Result<T> = {
|
||||
item: T;
|
||||
};
|
||||
|
|
|
@ -1,37 +1,42 @@
|
|||
import { BaseAPI, route } from "./base";
|
||||
import { BaseAPI } from "./base";
|
||||
import { ItemsApi } from "./classes/items";
|
||||
import { LabelsApi } from "./classes/labels";
|
||||
import { LocationsApi } from "./classes/locations";
|
||||
import { UserOut } from "./types/data-contracts";
|
||||
import { GroupApi } from "./classes/group";
|
||||
import { UserApi } from "./classes/users";
|
||||
import { Requests } from "~~/lib/requests";
|
||||
|
||||
export type Result<T> = {
|
||||
item: T;
|
||||
};
|
||||
|
||||
export class UserApi extends BaseAPI {
|
||||
export class UserClient extends BaseAPI {
|
||||
locations: LocationsApi;
|
||||
labels: LabelsApi;
|
||||
items: ItemsApi;
|
||||
group: GroupApi;
|
||||
user: UserApi;
|
||||
|
||||
constructor(requests: Requests) {
|
||||
super(requests);
|
||||
|
||||
this.locations = new LocationsApi(requests);
|
||||
this.labels = new LabelsApi(requests);
|
||||
this.items = new ItemsApi(requests);
|
||||
this.group = new GroupApi(requests);
|
||||
this.user = new UserApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/** @deprecated use this.user.self() */
|
||||
public self() {
|
||||
return this.http.get<Result<UserOut>>({ url: route("/users/self") });
|
||||
return this.user.self();
|
||||
}
|
||||
|
||||
/** @deprecated use this.user.logout() */
|
||||
public logout() {
|
||||
return this.http.post<object, void>({ url: route("/users/logout") });
|
||||
return this.user.logout();
|
||||
}
|
||||
|
||||
/** @deprecated use this.user.delete() */
|
||||
public deleteAccount() {
|
||||
return this.http.delete<void>({ url: route("/users/self") });
|
||||
return this.user.delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@nuxtjs/eslint-config-typescript": "^11.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
|
|
|
@ -16,14 +16,12 @@
|
|||
const auth = useAuthStore();
|
||||
|
||||
if (auth.self === null) {
|
||||
const { data, error } = await api.self();
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
auth.$patch({ self: data.item });
|
||||
|
||||
console.log(auth.self);
|
||||
}
|
||||
|
||||
const itemsStore = useItemStore();
|
||||
|
@ -140,7 +138,7 @@
|
|||
Import
|
||||
</button>
|
||||
</div>
|
||||
<BaseButton type="button" size="sm">
|
||||
<BaseButton type="button" size="sm" to="/profile">
|
||||
<Icon class="h-5 w-5 mr-2" name="mdi-person" />
|
||||
Profile
|
||||
</BaseButton>
|
||||
|
@ -148,12 +146,12 @@
|
|||
</template>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 divide-y divide-gray-300 border-t border-gray-300 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
|
||||
class="grid grid-cols-1 divide-y divide-base-300 border-t border-base-300 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
|
||||
>
|
||||
<div v-for="stat in stats" :key="stat.label" class="px-6 py-5 text-center text-sm font-medium">
|
||||
<span class="text-gray-900">{{ stat.value.value }}</span>
|
||||
<span class="text-base-900 font-bold">{{ stat.value.value }}</span>
|
||||
{{ " " }}
|
||||
<span class="text-gray-600">{{ stat.label }}</span>
|
||||
<span class="text-base-600">{{ stat.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
|
|
@ -16,12 +16,34 @@
|
|||
navigateTo("/home");
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const username = ref("");
|
||||
const email = ref("");
|
||||
const groupName = ref("");
|
||||
const password = ref("");
|
||||
const canRegister = ref(false);
|
||||
|
||||
const groupToken = computed<string>({
|
||||
get() {
|
||||
const params = route.query.token;
|
||||
|
||||
if (typeof params === "string") {
|
||||
return params;
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
set(v) {
|
||||
router.push({
|
||||
query: {
|
||||
token: v,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function registerUser() {
|
||||
loading.value = true;
|
||||
const { error } = await api.register({
|
||||
|
@ -29,6 +51,7 @@
|
|||
email: email.value,
|
||||
password: password.value,
|
||||
groupName: groupName.value,
|
||||
token: groupToken.value,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
@ -42,6 +65,12 @@
|
|||
registerForm.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (groupToken.value !== "") {
|
||||
registerForm.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const loginPassword = ref("");
|
||||
|
||||
|
@ -57,6 +86,7 @@
|
|||
|
||||
toast.success("Logged in successfully");
|
||||
|
||||
// @ts-expect-error - expires is either a date or a string, need to figure out store typing
|
||||
authStore.$patch({
|
||||
token: data.token,
|
||||
expires: data.expiresAt,
|
||||
|
@ -122,7 +152,13 @@
|
|||
</h2>
|
||||
<FormTextField v-model="email" label="Set your email?" />
|
||||
<FormTextField v-model="username" label="What's your name?" />
|
||||
<FormTextField v-model="groupName" label="Name your group" />
|
||||
<FormTextField v-if="groupToken == ''" v-model="groupName" label="Name your group" />
|
||||
<div v-else class="pt-4 pb-1 text-center">
|
||||
<p>You're Joining an Existing Group!</p>
|
||||
<button type="button" class="text-xs underline" @click="groupToken = ''">
|
||||
Don't Want To Join a Group?
|
||||
</button>
|
||||
</div>
|
||||
<FormTextField v-model="password" label="Set your password" type="password" />
|
||||
<PasswordScore v-model:valid="canRegister" :password="password" />
|
||||
<div class="card-actions justify-end">
|
||||
|
|
|
@ -283,13 +283,13 @@
|
|||
|
||||
<section class="px-3">
|
||||
<div class="space-y-4">
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="card bg-base-100 shadow-xl sm:rounded-lg overflow-visible">
|
||||
<BaseSectionHeader v-if="item" class="p-5">
|
||||
<Icon name="mdi-package-variant" class="-mt-1 mr-2 text-gray-600" />
|
||||
<span class="text-gray-600">
|
||||
<Icon name="mdi-package-variant" class="-mt-1 mr-2 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">Quantity {{ item.quantity }}</p>
|
||||
<p class="text-sm text-base-content font-bold pb-0 mb-0">Quantity {{ item.quantity }}</p>
|
||||
<template #after>
|
||||
<div class="modal-action mt-3">
|
||||
<div class="mr-auto tooltip" data-tip="Hide the cruft! ">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { DateDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { ItemAttachment } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
definePageMeta({
|
||||
|
@ -64,15 +65,33 @@
|
|||
);
|
||||
});
|
||||
|
||||
const itemSummary = computed(() => {
|
||||
return {
|
||||
Description: item.value?.description || "",
|
||||
"Serial Number": item.value?.serialNumber || "",
|
||||
"Model Number": item.value?.modelNumber || "",
|
||||
Manufacturer: item.value?.manufacturer || "",
|
||||
Notes: item.value?.notes || "",
|
||||
Insured: item.value?.insured ? "Yes" : "No",
|
||||
};
|
||||
const itemDetails = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: "Description",
|
||||
text: item.value?.description,
|
||||
},
|
||||
{
|
||||
name: "Serial Number",
|
||||
text: item.value?.serialNumber,
|
||||
},
|
||||
{
|
||||
name: "Mode Number",
|
||||
text: item.value?.modelNumber,
|
||||
},
|
||||
{
|
||||
name: "Manufacturer",
|
||||
text: item.value?.manufacturer,
|
||||
},
|
||||
{
|
||||
name: "Insured",
|
||||
text: item.value?.insured ? "Yes" : "No",
|
||||
},
|
||||
{
|
||||
name: "Notes",
|
||||
text: item.value?.notes,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const showAttachments = computed(() => {
|
||||
|
@ -88,35 +107,34 @@
|
|||
);
|
||||
});
|
||||
|
||||
const itemAttachments = computed(() => {
|
||||
const val: Record<string, string> = {};
|
||||
const attachmentDetails = computed(() => {
|
||||
const details: Detail[] = [];
|
||||
|
||||
if (preferences.value.showEmpty) {
|
||||
return {
|
||||
Photos: "",
|
||||
Manuals: "",
|
||||
Warranty: "",
|
||||
Attachments: "",
|
||||
};
|
||||
}
|
||||
const push = (name: string) => {
|
||||
details.push({
|
||||
name,
|
||||
text: "",
|
||||
slot: name.toLowerCase(),
|
||||
});
|
||||
};
|
||||
|
||||
if (attachments.value.photos.length > 0) {
|
||||
val.Photos = "";
|
||||
}
|
||||
|
||||
if (attachments.value.manuals.length > 0) {
|
||||
val.Manuals = "";
|
||||
}
|
||||
|
||||
if (attachments.value.warranty.length > 0) {
|
||||
val.Warranty = "";
|
||||
push("Photos");
|
||||
}
|
||||
|
||||
if (attachments.value.attachments.length > 0) {
|
||||
val.Attachments = "";
|
||||
push("Attachments");
|
||||
}
|
||||
|
||||
return val;
|
||||
if (attachments.value.warranty.length > 0) {
|
||||
push("Warranty");
|
||||
}
|
||||
|
||||
if (attachments.value.manuals.length > 0) {
|
||||
push("Manuals");
|
||||
}
|
||||
|
||||
return details;
|
||||
});
|
||||
|
||||
const showWarranty = computed(() => {
|
||||
|
@ -127,17 +145,32 @@
|
|||
});
|
||||
|
||||
const warrantyDetails = computed(() => {
|
||||
const payload = {
|
||||
"Lifetime Warranty": item.value?.lifetimeWarranty ? "Yes" : "No",
|
||||
};
|
||||
const details: (Detail | DateDetail)[] = [
|
||||
{
|
||||
name: "Lifetime Warranty",
|
||||
text: item.value?.lifetimeWarranty ? "Yes" : "No",
|
||||
},
|
||||
];
|
||||
|
||||
if (showWarranty.value) {
|
||||
payload["Warranty Expires"] = item.value?.warrantyExpires || "";
|
||||
if (item.value?.lifetimeWarranty) {
|
||||
details.push({
|
||||
name: "Warranty Expires",
|
||||
text: "N/A",
|
||||
});
|
||||
} else {
|
||||
details.push({
|
||||
name: "Warranty Expires",
|
||||
text: item.value?.warrantyExpires,
|
||||
type: "date",
|
||||
});
|
||||
}
|
||||
|
||||
payload["Warranty Details"] = item.value?.warrantyDetails || "";
|
||||
details.push({
|
||||
name: "Warranty Details",
|
||||
text: item.value?.warrantyDetails || "",
|
||||
});
|
||||
|
||||
return payload;
|
||||
return details;
|
||||
});
|
||||
|
||||
const showPurchase = computed(() => {
|
||||
|
@ -147,28 +180,45 @@
|
|||
return item.value?.purchaseFrom || item.value?.purchasePrice;
|
||||
});
|
||||
|
||||
const purchaseDetails = computed(() => {
|
||||
return {
|
||||
"Purchased From": item.value?.purchaseFrom || "",
|
||||
"Purchased Price": item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
|
||||
"Purchased At": item.value?.purchaseTime || "",
|
||||
};
|
||||
const purchaseDetails = computed<(Detail | DateDetail)[]>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Purchase From",
|
||||
label: item.value?.purchaseFrom || "",
|
||||
},
|
||||
{
|
||||
name: "Purchase Price",
|
||||
text: item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
|
||||
},
|
||||
{
|
||||
name: "Purchase Date",
|
||||
text: item.value.purchaseTime,
|
||||
},
|
||||
] as (Detail | DateDetail)[];
|
||||
});
|
||||
|
||||
const showSold = computed(() => {
|
||||
if (preferences.value.showEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.value?.soldTo || item.value?.soldPrice;
|
||||
});
|
||||
|
||||
const soldDetails = computed(() => {
|
||||
return {
|
||||
"Sold To": item.value?.soldTo || "",
|
||||
"Sold Price": item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
|
||||
"Sold At": item.value?.soldTime || "",
|
||||
};
|
||||
const soldDetails = computed<Array<Detail>>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Sold To",
|
||||
text: item.value?.soldTo || "",
|
||||
},
|
||||
{
|
||||
name: "Sold Price",
|
||||
text: item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
|
||||
},
|
||||
{
|
||||
name: "Sold At",
|
||||
text: item.value?.soldTime || "",
|
||||
},
|
||||
] as Detail[];
|
||||
});
|
||||
|
||||
const confirm = useConfirm();
|
||||
|
@ -197,91 +247,95 @@
|
|||
<div class="form-control"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<BaseDetails :details="itemSummary">
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader v-if="item" class="pb-0">
|
||||
<Icon name="mdi-package-variant" class="mr-2 text-gray-600" />
|
||||
<span class="text-gray-600">
|
||||
{{ item.name }}
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ item ? item.name : "" }}
|
||||
</span>
|
||||
<p class="text-sm text-gray-600 font-bold pb-0 mb-0">
|
||||
{{ item.location.name }} - Quantity {{ item.quantity }}
|
||||
</p>
|
||||
<template #after>
|
||||
<template #description>
|
||||
<p class="text-sm text-base-content font-bold pb-0 mb-0">
|
||||
{{ item.location.name }} - Quantity {{ item.quantity }}
|
||||
</p>
|
||||
<div v-if="item.labels && item.labels.length > 0" class="flex flex-wrap gap-3 mt-3">
|
||||
<LabelChip v-for="label in item.labels" :key="label.id" class="badge-primary" :label="label" />
|
||||
</div>
|
||||
<div class="modal-action mt-3">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Show Empty </span>
|
||||
</label>
|
||||
<BaseButton size="sm" :to="`/item/${itemId}/edit`">
|
||||
<template #icon>
|
||||
<Icon name="mdi-pencil" />
|
||||
</template>
|
||||
Edit
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" @click="deleteItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-delete" />
|
||||
</template>
|
||||
Delete
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showAttachments" :details="itemAttachments">
|
||||
<template #title-actions>
|
||||
<div class="modal-action mt-0">
|
||||
<label class="label cursor-pointer mr-auto">
|
||||
<input v-model="preferences.showEmpty" type="checkbox" class="toggle toggle-primary" />
|
||||
<span class="label-text ml-4"> Show Empty </span>
|
||||
</label>
|
||||
<BaseButton size="sm" :to="`/item/${itemId}/edit`">
|
||||
<template #icon>
|
||||
<Icon name="mdi-pencil" />
|
||||
</template>
|
||||
Edit
|
||||
</BaseButton>
|
||||
<BaseButton size="sm" @click="deleteItem">
|
||||
<template #icon>
|
||||
<Icon name="mdi-delete" />
|
||||
</template>
|
||||
Delete
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<DetailsSection :details="itemDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showAttachments">
|
||||
<template #title> Attachments </template>
|
||||
<template #Manuals>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.manuals.length > 0"
|
||||
:attachments="attachments.manuals"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #Attachments>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.attachments.length > 0"
|
||||
:attachments="attachments.attachments"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #Warranty>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.warranty.length > 0"
|
||||
:attachments="attachments.warranty"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #Photos>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.photos.length > 0"
|
||||
:attachments="attachments.photos"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showPurchase" :details="purchaseDetails">
|
||||
<template #title> Purchase Details </template>
|
||||
<template #PurchasedAt>
|
||||
<DateTime :date="item.purchaseTime" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showWarranty" :details="warrantyDetails">
|
||||
<DetailsSection :details="attachmentDetails">
|
||||
<template #manuals>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.manuals.length > 0"
|
||||
:attachments="attachments.manuals"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #attachments>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.attachments.length > 0"
|
||||
:attachments="attachments.attachments"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #warranty>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.warranty.length > 0"
|
||||
:attachments="attachments.warranty"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
<template #photos>
|
||||
<ItemAttachmentsList
|
||||
v-if="attachments.photos.length > 0"
|
||||
:attachments="attachments.photos"
|
||||
:item-id="item.id"
|
||||
/>
|
||||
</template>
|
||||
</DetailsSection>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showPurchase">
|
||||
<template #title> Purchase </template>
|
||||
<DetailsSection :details="purchaseDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showWarranty">
|
||||
<template #title> Warranty </template>
|
||||
<template #WarrantyExpires>
|
||||
<DateTime :date="item.warrantyExpires" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<BaseDetails v-if="showSold" :details="soldDetails">
|
||||
<DetailsSection :details="warrantyDetails" />
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard v-if="showSold">
|
||||
<template #title> Sold </template>
|
||||
<template #SoldAt>
|
||||
<DateTime :date="item.soldTime" />
|
||||
</template>
|
||||
</BaseDetails>
|
||||
<DetailsSection :details="soldDetails" />
|
||||
</BaseCard>
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
|
|
@ -125,8 +125,8 @@
|
|||
<BaseCard class="mb-16">
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-tag" class="mr-2 text-gray-600" />
|
||||
<span class="text-gray-600">
|
||||
<Icon name="mdi-tag" class="mr-2 -mt-1 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ label ? label.name : "" }}
|
||||
</span>
|
||||
</BaseSectionHeader>
|
||||
|
|
|
@ -123,8 +123,8 @@
|
|||
<BaseCard class="mb-16">
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-map-marker" class="mr-2 text-gray-600" />
|
||||
<span class="text-gray-600">
|
||||
<Icon name="mdi-map-marker" class="mr-2 -mt-1 text-base-content" />
|
||||
<span class="text-base-content">
|
||||
{{ location ? location.name : "" }}
|
||||
</span>
|
||||
</BaseSectionHeader>
|
||||
|
|
293
frontend/pages/profile.vue
Normal file
293
frontend/pages/profile.vue
Normal file
|
@ -0,0 +1,293 @@
|
|||
<script setup lang="ts">
|
||||
import { Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { DaisyTheme } from "~~/composables/use-preferences";
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Profile",
|
||||
});
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
type ThemeOption = {
|
||||
label: string;
|
||||
value: DaisyTheme;
|
||||
};
|
||||
|
||||
const themes: ThemeOption[] = [
|
||||
{
|
||||
label: "Garden",
|
||||
value: "garden",
|
||||
},
|
||||
{
|
||||
label: "Light",
|
||||
value: "light",
|
||||
},
|
||||
{
|
||||
label: "Cupcake",
|
||||
value: "cupcake",
|
||||
},
|
||||
{
|
||||
label: "Bumblebee",
|
||||
value: "bumblebee",
|
||||
},
|
||||
{
|
||||
label: "Emerald",
|
||||
value: "emerald",
|
||||
},
|
||||
{
|
||||
label: "Corporate",
|
||||
value: "corporate",
|
||||
},
|
||||
{
|
||||
label: "Synthwave",
|
||||
value: "synthwave",
|
||||
},
|
||||
{
|
||||
label: "Retro",
|
||||
value: "retro",
|
||||
},
|
||||
{
|
||||
label: "Cyberpunk",
|
||||
value: "cyberpunk",
|
||||
},
|
||||
{
|
||||
label: "Valentine",
|
||||
value: "valentine",
|
||||
},
|
||||
{
|
||||
label: "Halloween",
|
||||
value: "halloween",
|
||||
},
|
||||
{
|
||||
label: "Forest",
|
||||
value: "forest",
|
||||
},
|
||||
{
|
||||
label: "Aqua",
|
||||
value: "aqua",
|
||||
},
|
||||
{
|
||||
label: "Lofi",
|
||||
value: "lofi",
|
||||
},
|
||||
{
|
||||
label: "Pastel",
|
||||
value: "pastel",
|
||||
},
|
||||
{
|
||||
label: "Fantasy",
|
||||
value: "fantasy",
|
||||
},
|
||||
{
|
||||
label: "Wireframe",
|
||||
value: "wireframe",
|
||||
},
|
||||
{
|
||||
label: "Black",
|
||||
value: "black",
|
||||
},
|
||||
{
|
||||
label: "Luxury",
|
||||
value: "luxury",
|
||||
},
|
||||
{
|
||||
label: "Dracula",
|
||||
value: "dracula",
|
||||
},
|
||||
{
|
||||
label: "Cmyk",
|
||||
value: "cmyk",
|
||||
},
|
||||
{
|
||||
label: "Autumn",
|
||||
value: "autumn",
|
||||
},
|
||||
{
|
||||
label: "Business",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
label: "Acid",
|
||||
value: "acid",
|
||||
},
|
||||
{
|
||||
label: "Lemonade",
|
||||
value: "lemonade",
|
||||
},
|
||||
{
|
||||
label: "Night",
|
||||
value: "night",
|
||||
},
|
||||
{
|
||||
label: "Coffee",
|
||||
value: "coffee",
|
||||
},
|
||||
{
|
||||
label: "Winter",
|
||||
value: "winter",
|
||||
},
|
||||
];
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const details = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: "Name",
|
||||
text: auth.self?.name || "Unknown",
|
||||
},
|
||||
{
|
||||
name: "Email",
|
||||
text: auth.self?.email || "Unknown",
|
||||
},
|
||||
] as Detail[];
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotifier();
|
||||
|
||||
async function deleteProfile() {
|
||||
const result = await confirm.open(
|
||||
"Are you sure you want to delete your account? If you are the last member in your group all your data will be deleted. This action cannot be undone."
|
||||
);
|
||||
|
||||
if (result.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { response } = await api.user.delete();
|
||||
|
||||
if (response?.status === 204) {
|
||||
notify.success("Your account has been deleted.");
|
||||
auth.logout(api);
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
notify.error("Failed to delete your account.");
|
||||
}
|
||||
|
||||
const token = ref("");
|
||||
const tokenUrl = computed(() => {
|
||||
if (!window) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${window.location.origin}?token=${token.value}`;
|
||||
});
|
||||
|
||||
async function generateToken() {
|
||||
const date = new Date();
|
||||
|
||||
const { response, data } = await api.group.createInvitation({
|
||||
expiresAt: new Date(date.setDate(date.getDate() + 7)),
|
||||
uses: 1,
|
||||
});
|
||||
|
||||
if (response?.status === 201) {
|
||||
token.value = data.token;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer class="flex flex-col gap-4 mb-6">
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-account" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> User Profile </span>
|
||||
<template #description> Invite users, and manage your account. </template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<DetailsSection :details="details" />
|
||||
|
||||
<div class="p-4">
|
||||
<div class="flex gap-2">
|
||||
<BaseButton size="sm"> Change Password </BaseButton>
|
||||
<BaseButton size="sm" @click="generateToken"> Generate Invite Link </BaseButton>
|
||||
</div>
|
||||
<div v-if="token" class="pt-4 flex items-center pl-1">
|
||||
<CopyText class="mr-2 btn-primary" :text="tokenUrl" />
|
||||
{{ tokenUrl }}
|
||||
</div>
|
||||
<div v-if="token" class="pt-4 flex items-center pl-1">
|
||||
<CopyText class="mr-2 btn-primary" :text="token" />
|
||||
{{ token }}
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-fill" class="mr-2 text-base-600" />
|
||||
<span class="text-base-600"> Theme Settings </span>
|
||||
<template #description>
|
||||
Theme settings are stored in your browser's local storage. You can change the theme at any time. If you're
|
||||
having trouble setting your theme try refreshing your browser.
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<div class="px-4 pb-4">
|
||||
<div class="rounded-box grid grid-cols-1 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
|
||||
<div
|
||||
v-for="theme in themes"
|
||||
:key="theme.value"
|
||||
class="border-base-content/20 hover:border-base-content/40 outline-base-content overflow-hidden rounded-lg border outline-2 outline-offset-2"
|
||||
:data-theme="theme.value"
|
||||
:data-set-theme="theme.value"
|
||||
data-act-class="outline"
|
||||
@click="setTheme(theme.value)"
|
||||
>
|
||||
<div :data-theme="theme.value" class="bg-base-100 text-base-content w-full cursor-pointer font-sans">
|
||||
<div class="grid grid-cols-5 grid-rows-3">
|
||||
<div class="bg-base-200 col-start-1 row-span-2 row-start-1"></div>
|
||||
<div class="bg-base-300 col-start-1 row-start-3"></div>
|
||||
<div class="bg-base-100 col-span-4 col-start-2 row-span-3 row-start-1 flex flex-col gap-1 p-2">
|
||||
<div class="font-bold">{{ theme.label }}</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<div class="bg-primary flex aspect-square w-5 items-center justify-center rounded lg:w-6">
|
||||
<div class="text-primary-content text-sm font-bold">A</div>
|
||||
</div>
|
||||
<div class="bg-secondary flex aspect-square w-5 items-center justify-center rounded lg:w-6">
|
||||
<div class="text-secondary-content text-sm font-bold">A</div>
|
||||
</div>
|
||||
<div class="bg-accent flex aspect-square w-5 items-center justify-center rounded lg:w-6">
|
||||
<div class="text-accent-content text-sm font-bold">A</div>
|
||||
</div>
|
||||
<div class="bg-neutral flex aspect-square w-5 items-center justify-center rounded lg:w-6">
|
||||
<div class="text-neutral-content text-sm font-bold">A</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
<Icon name="mdi-delete" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Delete Account</span>
|
||||
<template #description> Delete your account and all it's associated data </template>
|
||||
</BaseSectionHeader>
|
||||
|
||||
<div class="py-4 border-t-2 border-gray-300">
|
||||
<BaseButton class="btn-error" @click="deleteProfile"> Delete Account </BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
</BaseCard>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
7
frontend/pnpm-lock.yaml
generated
7
frontend/pnpm-lock.yaml
generated
|
@ -1,6 +1,7 @@
|
|||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@faker-js/faker': ^7.5.0
|
||||
'@iconify/vue': ^3.2.1
|
||||
'@nuxtjs/eslint-config-typescript': ^11.0.0
|
||||
'@nuxtjs/tailwindcss': ^5.3.2
|
||||
|
@ -44,6 +45,7 @@ dependencies:
|
|||
vue: 3.2.39
|
||||
|
||||
devDependencies:
|
||||
'@faker-js/faker': 7.5.0
|
||||
'@nuxtjs/eslint-config-typescript': 11.0.0_7ilbxdl5iguzcjriqqcg2m5cku
|
||||
'@typescript-eslint/eslint-plugin': 5.38.0_4gkcvl6qsi23tqqawfqgcwtp54
|
||||
'@typescript-eslint/parser': 5.38.0_7ilbxdl5iguzcjriqqcg2m5cku
|
||||
|
@ -381,6 +383,11 @@ packages:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/@faker-js/faker/7.5.0:
|
||||
resolution: {integrity: sha512-8wNUCCUHvfvI0gQpDUho/3gPzABffnCn5um65F8dzQ86zz6dlt4+nmAA7PQUc8L+eH+9RgR/qzy5N/8kN0Ozdw==}
|
||||
engines: {node: '>=14.0.0', npm: '>=6.0.0'}
|
||||
dev: true
|
||||
|
||||
/@humanwhocodes/config-array/0.10.5:
|
||||
resolution: {integrity: sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { UserApi } from "~~/lib/api/user";
|
||||
import { UserClient } from "~~/lib/api/user";
|
||||
import { UserOut } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
|
@ -23,12 +23,8 @@ export const useAuthStore = defineStore("auth", {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
async logout(api: UserApi) {
|
||||
const result = await api.logout();
|
||||
|
||||
if (result.error) {
|
||||
return result;
|
||||
}
|
||||
async logout(api: UserClient) {
|
||||
const result = await api.user.logout();
|
||||
|
||||
this.token = "";
|
||||
this.expires = "";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue