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
|
@ -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",
|
||||
},
|
||||
];
|
Loading…
Add table
Add a link
Reference in a new issue