forked from mirrors/homebox
feat: currency selection support (#72)
* initial UI for currency selection * add task to purge invitation tokens * group API contracts * fix type import * use auth middleware * add currency setting support (UI) * use group settings for format currency * fix casing
This commit is contained in:
parent
1cc38d6a5c
commit
461be2afca
40 changed files with 930 additions and 343 deletions
|
@ -25,7 +25,7 @@
|
|||
},
|
||||
modelValue: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: [Object, String, Boolean] as any,
|
||||
type: [Object, String] as any,
|
||||
default: null,
|
||||
},
|
||||
items: {
|
||||
|
@ -53,10 +53,19 @@
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function compare(a: any, b: any): boolean {
|
||||
if (props.value != null) {
|
||||
if (props.value) {
|
||||
return a[props.value] === b[props.value];
|
||||
}
|
||||
return a === b;
|
||||
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!a || !b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
watch(
|
||||
|
|
22
frontend/components/global/Currency.vue
Normal file
22
frontend/components/global/Currency.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
{{ value }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
amount: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const fmt = await useFormatCurrency();
|
||||
|
||||
const value = computed(() => {
|
||||
if (!props.amount || props.amount === "0") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return fmt(props.amount);
|
||||
});
|
||||
</script>
|
|
@ -7,9 +7,8 @@
|
|||
</dt>
|
||||
<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" />
|
||||
</template>
|
||||
<DateTime v-if="detail.type == 'date'" :date="detail.text" />
|
||||
<Currency v-else-if="detail.type == 'currency'" :amount="detail.text" />
|
||||
<template v-else>
|
||||
{{ detail.text }}
|
||||
</template>
|
||||
|
@ -21,11 +20,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DateDetail, Detail } from "./types";
|
||||
import type { CustomDetail, Detail } from "./types";
|
||||
|
||||
defineProps({
|
||||
details: {
|
||||
type: Object as () => (Detail | DateDetail)[],
|
||||
type: Object as () => (Detail | CustomDetail)[],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
export type StringLike = string | number | boolean;
|
||||
|
||||
export type DateDetail = {
|
||||
type BaseDetail = {
|
||||
name: string;
|
||||
text: string | Date;
|
||||
slot?: string;
|
||||
type: "date";
|
||||
};
|
||||
|
||||
export type Detail = {
|
||||
name: string;
|
||||
type DateDetail = BaseDetail & {
|
||||
type: "date";
|
||||
text: Date | string;
|
||||
};
|
||||
|
||||
type CurrencyDetail = BaseDetail & {
|
||||
type: "currency";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type CustomDetail = DateDetail | CurrencyDetail;
|
||||
|
||||
export type Detail = BaseDetail & {
|
||||
text: StringLike;
|
||||
slot?: string;
|
||||
type?: "text";
|
||||
};
|
||||
|
||||
export type Details = Array<Detail | CustomDetail>;
|
||||
|
|
21
frontend/composables/use-formatters.ts
Normal file
21
frontend/composables/use-formatters.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
const cache = {
|
||||
currency: "",
|
||||
};
|
||||
|
||||
export function ResetCurrency() {
|
||||
cache.currency = "";
|
||||
}
|
||||
|
||||
export async function useFormatCurrency() {
|
||||
if (!cache.currency) {
|
||||
const client = useUserApi();
|
||||
|
||||
const { data: group } = await client.group.get();
|
||||
|
||||
if (group) {
|
||||
cache.currency = group.currency;
|
||||
}
|
||||
}
|
||||
|
||||
return (value: number | string) => fmtCurrency(value, cache.currency);
|
||||
}
|
|
@ -1,35 +1,5 @@
|
|||
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";
|
||||
import { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export type LocationViewPreferences = {
|
||||
showDetails: boolean;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ComputedRef } from "vue";
|
||||
import { DaisyTheme } from "./use-preferences";
|
||||
import { DaisyTheme } from "~~/lib/data/themes";
|
||||
|
||||
export interface UseTheme {
|
||||
theme: ComputedRef<DaisyTheme>;
|
||||
|
|
|
@ -1,10 +1,70 @@
|
|||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
<div>
|
||||
<AppToast />
|
||||
<AppHeader />
|
||||
<main class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen">
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useItemStore } from "~~/stores/items";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
/**
|
||||
* Store Provider Initialization
|
||||
*/
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const reLabel = /\/api\/v1\/labels\/.*/gm;
|
||||
const rmLabelStoreObserver = defineObserver("labelStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLabel)) {
|
||||
labelStore.refresh();
|
||||
}
|
||||
console.debug("labelStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const reLocation = /\/api\/v1\/locations\/.*/gm;
|
||||
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLocation)) {
|
||||
locationStore.refresh();
|
||||
}
|
||||
console.debug("locationStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const itemStore = useItemStore();
|
||||
const reItem = /\/api\/v1\/items\/.*/gm;
|
||||
const rmItemStoreObserver = defineObserver("itemStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reItem)) {
|
||||
itemStore.refresh();
|
||||
}
|
||||
console.debug("itemStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const eventBus = useEventBus();
|
||||
eventBus.on(
|
||||
EventTypes.ClearStores,
|
||||
() => {
|
||||
labelStore.refresh();
|
||||
itemStore.refresh();
|
||||
locationStore.refresh();
|
||||
},
|
||||
"stores"
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
rmLabelStoreObserver();
|
||||
rmLocationStoreObserver();
|
||||
rmItemStoreObserver();
|
||||
eventBus.off(EventTypes.ClearStores, "stores");
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<AppToast />
|
||||
<AppHeader />
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useItemStore } from "~~/stores/items";
|
||||
import { useLabelStore } from "~~/stores/labels";
|
||||
import { useLocationStore } from "~~/stores/locations";
|
||||
/**
|
||||
* Store Provider Initialization
|
||||
*/
|
||||
|
||||
const labelStore = useLabelStore();
|
||||
const reLabel = /\/api\/v1\/labels\/.*/gm;
|
||||
const rmLabelStoreObserver = defineObserver("labelStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLabel)) {
|
||||
labelStore.refresh();
|
||||
}
|
||||
console.debug("labelStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const locationStore = useLocationStore();
|
||||
const reLocation = /\/api\/v1\/locations\/.*/gm;
|
||||
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reLocation)) {
|
||||
locationStore.refresh();
|
||||
}
|
||||
console.debug("locationStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const itemStore = useItemStore();
|
||||
const reItem = /\/api\/v1\/items\/.*/gm;
|
||||
const rmItemStoreObserver = defineObserver("itemStore", {
|
||||
handler: r => {
|
||||
if (r.status === 201 || r.url.match(reItem)) {
|
||||
itemStore.refresh();
|
||||
}
|
||||
console.debug("itemStore handler called by observer");
|
||||
},
|
||||
});
|
||||
|
||||
const eventBus = useEventBus();
|
||||
eventBus.on(
|
||||
EventTypes.ClearStores,
|
||||
() => {
|
||||
labelStore.refresh();
|
||||
itemStore.refresh();
|
||||
locationStore.refresh();
|
||||
},
|
||||
"stores"
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
rmLabelStoreObserver();
|
||||
rmLocationStoreObserver();
|
||||
rmItemStoreObserver();
|
||||
eventBus.off(EventTypes.ClearStores, "stores");
|
||||
});
|
||||
</script>
|
|
@ -1,6 +1,5 @@
|
|||
import { describe, test, expect } from "vitest";
|
||||
import { factories } from "./factories";
|
||||
import { sharedUserClient } from "./test-utils";
|
||||
|
||||
describe("[GET] /api/v1/status", () => {
|
||||
test("server should respond", async () => {
|
||||
|
@ -32,43 +31,4 @@ describe("first time user workflow (register, login, join group)", () => {
|
|||
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 = factories.user();
|
||||
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 = factories.client.user(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);
|
||||
});
|
||||
});
|
||||
|
|
66
frontend/lib/api/__test__/user/group.test.ts
Normal file
66
frontend/lib/api/__test__/user/group.test.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { factories } from "../factories";
|
||||
import { sharedUserClient } from "../test-utils";
|
||||
|
||||
describe("first time user workflow (register, login, join group)", () => {
|
||||
test("user should be able to update group", async () => {
|
||||
const { client } = await factories.client.singleUse();
|
||||
|
||||
const name = faker.name.firstName();
|
||||
|
||||
const { response, data: group } = await client.group.update({
|
||||
name,
|
||||
currency: "eur",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(group.name).toBe(name);
|
||||
});
|
||||
|
||||
test("user should be able to get own group", async () => {
|
||||
const { client } = await factories.client.singleUse();
|
||||
|
||||
const { response, data: group } = await client.group.get();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(group.name).toBeTruthy();
|
||||
expect(group.currency).toBe("USD");
|
||||
});
|
||||
|
||||
test("user should be able to join create join token and have user signup", async () => {
|
||||
const api = factories.client.public();
|
||||
|
||||
// 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 = factories.user();
|
||||
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 = factories.client.user(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);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseAPI, route } from "../base";
|
||||
import { GroupInvitation, GroupInvitationCreate } from "../types/data-contracts";
|
||||
import { Group, GroupInvitation, GroupInvitationCreate, GroupUpdate } from "../types/data-contracts";
|
||||
|
||||
export class GroupApi extends BaseAPI {
|
||||
createInvitation(data: GroupInvitationCreate) {
|
||||
|
@ -8,4 +8,17 @@ export class GroupApi extends BaseAPI {
|
|||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
update(data: GroupUpdate) {
|
||||
return this.http.put<GroupUpdate, Group>({
|
||||
url: route("/groups"),
|
||||
body: data,
|
||||
});
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.http.get<Group>({
|
||||
url: route("/groups"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,19 @@ export interface DocumentOut {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
createdAt: Date;
|
||||
currency: string;
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface GroupUpdate {
|
||||
currency: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ItemAttachment {
|
||||
createdAt: Date;
|
||||
document: DocumentOut;
|
||||
|
|
35
frontend/lib/data/currency.ts
Normal file
35
frontend/lib/data/currency.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export type Codes = "USD" | "EUR" | "GBP" | "JPY";
|
||||
|
||||
export type Currency = {
|
||||
code: Codes;
|
||||
local: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const currencies: Currency[] = [
|
||||
{
|
||||
code: "USD",
|
||||
local: "en-US",
|
||||
symbol: "$",
|
||||
name: "US Dollar",
|
||||
},
|
||||
{
|
||||
code: "EUR",
|
||||
local: "de-DE",
|
||||
symbol: "€",
|
||||
name: "Euro",
|
||||
},
|
||||
{
|
||||
code: "GBP",
|
||||
local: "en-GB",
|
||||
symbol: "£",
|
||||
name: "British Pound",
|
||||
},
|
||||
{
|
||||
code: "JPY",
|
||||
local: "ja-JP",
|
||||
symbol: "¥",
|
||||
name: "Japanese Yen",
|
||||
},
|
||||
];
|
150
frontend/lib/data/themes.ts
Normal file
150
frontend/lib/data/themes.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
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 ThemeOption = {
|
||||
label: string;
|
||||
value: DaisyTheme;
|
||||
};
|
||||
|
||||
export 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",
|
||||
},
|
||||
];
|
15
frontend/middleware/auth.ts
Normal file
15
frontend/middleware/auth.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { useAuthStore } from "~~/stores/auth";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
const auth = useAuthStore();
|
||||
const api = useUserApi();
|
||||
|
||||
if (!auth.self) {
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
auth.$patch({ self: data.item });
|
||||
}
|
||||
});
|
|
@ -5,8 +5,9 @@
|
|||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
@ -15,15 +16,6 @@
|
|||
|
||||
const auth = useAuthStore();
|
||||
|
||||
if (auth.self === null) {
|
||||
const { data, error } = await api.user.self();
|
||||
if (error) {
|
||||
navigateTo("/");
|
||||
}
|
||||
|
||||
auth.$patch({ self: data.item });
|
||||
}
|
||||
|
||||
const itemsStore = useItemStore();
|
||||
const items = computed(() => itemsStore.items);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { capitalize } from "~~/lib/strings";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { DateDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { Detail, Details } from "~~/components/global/DetailsSection/types";
|
||||
import { ItemAttachment } from "~~/lib/api/types/data-contracts";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
@ -145,7 +145,7 @@
|
|||
});
|
||||
|
||||
const warrantyDetails = computed(() => {
|
||||
const details: (Detail | DateDetail)[] = [
|
||||
const details: Details = [
|
||||
{
|
||||
name: "Lifetime Warranty",
|
||||
text: item.value?.lifetimeWarranty ? "Yes" : "No",
|
||||
|
@ -180,7 +180,7 @@
|
|||
return item.value?.purchaseFrom || item.value?.purchasePrice !== "0";
|
||||
});
|
||||
|
||||
const purchaseDetails = computed<Array<Detail | DateDetail>>(() => {
|
||||
const purchaseDetails = computed<Details>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Purchased From",
|
||||
|
@ -188,7 +188,8 @@
|
|||
},
|
||||
{
|
||||
name: "Purchase Price",
|
||||
text: item.value?.purchasePrice ? fmtCurrency(item.value.purchasePrice) : "",
|
||||
text: item.value?.purchasePrice || "",
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
name: "Purchase Date",
|
||||
|
@ -205,7 +206,7 @@
|
|||
return item.value?.soldTo || item.value?.soldPrice !== "0";
|
||||
});
|
||||
|
||||
const soldDetails = computed<Array<Detail | DateDetail>>(() => {
|
||||
const soldDetails = computed<Details>(() => {
|
||||
return [
|
||||
{
|
||||
name: "Sold To",
|
||||
|
@ -213,7 +214,8 @@
|
|||
},
|
||||
{
|
||||
name: "Sold Price",
|
||||
text: item.value?.soldPrice ? fmtCurrency(item.value.soldPrice) : "",
|
||||
text: item.value?.soldPrice || "",
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
name: "Sold At",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const show = reactive({
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
import { useLocationStore } from "~~/stores/locations";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Homebox | Home",
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import type { DateDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
import type { CustomDetail, Detail } from "~~/components/global/DetailsSection/types";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
@ -23,7 +23,7 @@
|
|||
return data;
|
||||
});
|
||||
|
||||
const details = computed<(Detail | DateDetail)[]>(() => {
|
||||
const details = computed<(Detail | CustomDetail)[]>(() => {
|
||||
const details = [
|
||||
{
|
||||
name: "Name",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { Detail, DateDetail } from "~~/components/global/DetailsSection/types";
|
||||
import { Detail, CustomDetail } from "~~/components/global/DetailsSection/types";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
@ -23,7 +23,7 @@
|
|||
return data;
|
||||
});
|
||||
|
||||
const details = computed<(Detail | DateDetail)[]>(() => {
|
||||
const details = computed<(Detail | CustomDetail)[]>(() => {
|
||||
const details = [
|
||||
{
|
||||
name: "Name",
|
||||
|
|
|
@ -1,15 +1,70 @@
|
|||
<script setup lang="ts">
|
||||
import { Detail } from "~~/components/global/DetailsSection/types";
|
||||
import { DaisyTheme } from "~~/composables/use-preferences";
|
||||
import { useAuthStore } from "~~/stores/auth";
|
||||
import { themes } from "~~/lib/data/themes";
|
||||
import { currencies, Currency } from "~~/lib/data/currency";
|
||||
|
||||
definePageMeta({
|
||||
layout: "home",
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Profile",
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotifier();
|
||||
|
||||
// Currency Selection
|
||||
const currency = ref<Currency>(currencies[0]);
|
||||
|
||||
watch(currency, () => {
|
||||
if (group.value) {
|
||||
group.value.currency = currency.value.code;
|
||||
}
|
||||
|
||||
console.log(group.value);
|
||||
});
|
||||
|
||||
const currencyExample = computed(() => {
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency.value ? currency.value.code : "USD",
|
||||
});
|
||||
|
||||
return formatter.format(1000);
|
||||
});
|
||||
|
||||
const { data: group } = useAsyncData(async () => {
|
||||
const { data } = await api.group.get();
|
||||
return data;
|
||||
});
|
||||
|
||||
// Sync Initial Currency
|
||||
watch(group, () => {
|
||||
if (group.value) {
|
||||
const found = currencies.find(c => c.code === group.value.currency);
|
||||
if (found) {
|
||||
currency.value = found;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function updateGroup() {
|
||||
const { data, error } = await api.group.update({
|
||||
name: group.value.name,
|
||||
currency: group.value.currency,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notify.error("Failed to update group");
|
||||
return;
|
||||
}
|
||||
|
||||
group.value = data;
|
||||
notify.success("Group updated");
|
||||
}
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data } = await pubApi.status();
|
||||
|
@ -19,126 +74,6 @@
|
|||
|
||||
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(() => {
|
||||
|
@ -154,10 +89,6 @@
|
|||
] 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."
|
||||
|
@ -283,6 +214,27 @@
|
|||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader class="pb-0">
|
||||
<Icon name="mdi-accounts" class="mr-2 -mt-1 text-base-600" />
|
||||
<span class="text-base-600"> Group Settings </span>
|
||||
<template #description>
|
||||
Shared Group Settings. You may need to refresh your browser for some settings to apply.
|
||||
</template>
|
||||
</BaseSectionHeader>
|
||||
</template>
|
||||
|
||||
<div v-if="group" class="p-5 pt-0">
|
||||
<FormSelect v-model="currency" value="code" label="Currency Format" :items="currencies" />
|
||||
<p class="m-2 text-sm">Example: {{ currencyExample }}</p>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<BaseButton @click="updateGroup"> Update Group </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
<BaseCard>
|
||||
<template #title>
|
||||
<BaseSectionHeader>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue