-
-
+
-
{{ detail.name }}
- -
+
-
diff --git a/frontend/composables/use-api.ts b/frontend/composables/use-api.ts
index 4eb753f..69da2ed 100644
--- a/frontend/composables/use-api.ts
+++ b/frontend/composables/use-api.ts
@@ -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);
}
diff --git a/frontend/composables/use-preferences.ts b/frontend/composables/use-preferences.ts
index c1e8f66..6c31fec 100644
--- a/frontend/composables/use-preferences.ts
+++ b/frontend/composables/use-preferences.ts
@@ -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 {
showDetails: true,
showEmpty: true,
editorSimpleView: true,
+ theme: "garden",
},
{ mergeDefaults: true }
);
diff --git a/frontend/composables/use-theme.ts b/frontend/composables/use-theme.ts
new file mode 100644
index 0000000..16c6ca1
--- /dev/null
+++ b/frontend/composables/use-theme.ts
@@ -0,0 +1,40 @@
+import { ComputedRef } from "vue";
+import { DaisyTheme } from "./use-preferences";
+
+export interface UseTheme {
+ theme: ComputedRef;
+ setTheme: (theme: DaisyTheme) => void;
+}
+
+const themeRef = ref("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(null);
+
+ onMounted(() => {
+ if (htmlEl.value) {
+ return;
+ }
+
+ htmlEl.value = document.querySelector("html");
+ });
+
+ const theme = computed(() => {
+ return themeRef.value;
+ });
+
+ return { theme, setTheme };
+}
diff --git a/frontend/lib/api/__test__/public.test.ts b/frontend/lib/api/__test__/public.test.ts
index 6dd618e..cb3a9c3 100644
--- a/frontend/lib/api/__test__/public.test.ts
+++ b/frontend/lib/api/__test__/public.test.ts
@@ -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);
+ });
});
diff --git a/frontend/lib/api/__test__/test-utils.ts b/frontend/lib/api/__test__/test-utils.ts
index 0be76d2..cab9b9a 100644
--- a/frontend/lib/api/__test__/test-utils.ts
+++ b/frontend/lib/api/__test__/test-utils.ts
@@ -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 {
+export async function sharedUserClient(): Promise {
if (cache.token) {
return userClient(cache.token);
}
diff --git a/frontend/lib/api/__test__/user/items.test.ts b/frontend/lib/api/__test__/user/items.test.ts
index 75ca5cf..9bdea4d 100644
--- a/frontend/lib/api/__test__/user/items.test.ts
+++ b/frontend/lib/api/__test__/user/items.test.ts
@@ -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]> {
+ async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise]> {
const { response, data } = await api.locations.create({
name: `__test__.location.name_${increment}`,
description: `__test__.location.description_${increment}`,
diff --git a/frontend/lib/api/__test__/user/labels.test.ts b/frontend/lib/api/__test__/user/labels.test.ts
index b2b7e0d..1fc414a 100644
--- a/frontend/lib/api/__test__/user/labels.test.ts
+++ b/frontend/lib/api/__test__/user/labels.test.ts
@@ -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]> {
+ async function useLabel(api: UserClient): Promise<[LabelOut, () => Promise]> {
const { response, data } = await api.labels.create({
name: `__test__.label.name_${increment}`,
description: `__test__.label.description_${increment}`,
diff --git a/frontend/lib/api/__test__/user/locations.test.ts b/frontend/lib/api/__test__/user/locations.test.ts
index 0e6e9af..8583074 100644
--- a/frontend/lib/api/__test__/user/locations.test.ts
+++ b/frontend/lib/api/__test__/user/locations.test.ts
@@ -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]> {
+ async function useLocation(api: UserClient): Promise<[LocationOut, () => Promise]> {
const { response, data } = await api.locations.create({
name: `__test__.location.name_${increment}`,
description: `__test__.location.description_${increment}`,
diff --git a/frontend/lib/api/classes/group.ts b/frontend/lib/api/classes/group.ts
new file mode 100644
index 0000000..5a687f1
--- /dev/null
+++ b/frontend/lib/api/classes/group.ts
@@ -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({
+ url: route("/groups/invitations"),
+ body: data,
+ });
+ }
+}
diff --git a/frontend/lib/api/classes/users.ts b/frontend/lib/api/classes/users.ts
new file mode 100644
index 0000000..39f03b4
--- /dev/null
+++ b/frontend/lib/api/classes/users.ts
@@ -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>({ url: route("/users/self") });
+ }
+
+ public logout() {
+ return this.http.post
-
+
Profile
@@ -148,12 +146,12 @@
- {{ stat.value.value }}
+ {{ stat.value.value }}
{{ " " }}
- {{ stat.label }}
+ {{ stat.label }}
diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue
index 9b41094..5194198 100644
--- a/frontend/pages/index.vue
+++ b/frontend/pages/index.vue
@@ -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({
+ 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 @@
-
+
+
+
You're Joining an Existing Group!
+
+
diff --git a/frontend/pages/item/[id]/edit.vue b/frontend/pages/item/[id]/edit.vue
index e064803..b1a2d43 100644
--- a/frontend/pages/item/[id]/edit.vue
+++ b/frontend/pages/item/[id]/edit.vue
@@ -283,13 +283,13 @@
-
+
-
-
+
+
{{ item.name }}
- Quantity {{ item.quantity }}
+ Quantity {{ item.quantity }}