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:
Hayden 2022-10-15 12:15:55 -08:00 committed by GitHub
parent 1cc38d6a5c
commit 461be2afca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 930 additions and 343 deletions

View file

@ -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);
});
});

View 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);
});
});

View file

@ -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"),
});
}
}

View file

@ -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;

View 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
View 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",
},
];