Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/api/controllers/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MonkeyRequest } from "../types";
export async function getConfiguration(
_req: MonkeyRequest,
): Promise<GetConfigurationResponse> {
const currentConfiguration = await Configuration.getLiveConfiguration();
const currentConfiguration = await Configuration.getCachedConfiguration(true);
return new MonkeyResponse("Configuration retrieved", currentConfiguration);
}

Expand Down
10 changes: 8 additions & 2 deletions backend/src/api/controllers/psa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import * as PsaDAL from "../../dal/psa";
import { MonkeyResponse } from "../../utils/monkey-response";
import { replaceObjectIds } from "../../utils/misc";
import { MonkeyRequest } from "../types";
import { PSA } from "@monkeytype/schemas/psas";
import { cacheWithTTL } from "../../utils/ttl-cache";

//cache for one minute
const cache = cacheWithTTL<PSA[]>(1 * 60 * 1000, async () => {
return replaceObjectIds(await PsaDAL.get());
});

export async function getPsas(_req: MonkeyRequest): Promise<GetPsaResponse> {
const data = await PsaDAL.get();
return new MonkeyResponse("PSAs retrieved", replaceObjectIds(data));
return new MonkeyResponse("PSAs retrieved", (await cache()) ?? []);
}
27 changes: 27 additions & 0 deletions backend/src/utils/ttl-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Creates a caching function that loads data with a specified TTL (Time-to-Live).
* If the cache has expired (based on TTL), it will re-fetch the data by calling the provided function.
* Otherwise, it returns the cached value.
*
* @template T - The type of the value being cached.
*
* @param {number} ttlMs - The Time-to-Live (TTL) in milliseconds. The cache will refetch on call after this duration.
* @param {() => Promise<T>} fn - A function that returns a promise resolving to the data to cache.
*
* @returns {() => Promise<T | undefined>}
*/
export function cacheWithTTL<T>(
ttlMs: number,
fn: () => Promise<T>,
): () => Promise<T | undefined> {
let lastFetchTime = 0;
let cache: T | undefined;

return async () => {
if (lastFetchTime < Date.now() - ttlMs) {
lastFetchTime = Date.now();
cache = await fn();
}
return cache;
};
}
151 changes: 151 additions & 0 deletions frontend/__tests__/components/ui/table/DataTable.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { render, screen, fireEvent } from "@solidjs/testing-library";
import { createSignal } from "solid-js";
import { describe, it, expect, vi, beforeEach } from "vitest";

import { DataTable } from "../../../../src/ts/components/ui/table/DataTable";

const [localStorage, setLocalStorage] = createSignal([]);
vi.mock("../../../../src/ts/hooks/useLocalStorage", () => {
return {
useLocalStorage: () => {
return [localStorage, setLocalStorage] as const;
},
};
});

const bpSignal = createSignal({
xxs: true,
sm: true,
md: true,
});

vi.mock("../../../../src/ts/signals/breakpoints", () => ({
bp: () => bpSignal[0](),
}));

type Person = {
name: string;
age: number;
};

const columns = [
{
accessorKey: "name",
header: "Name",
cell: (info: any) => info.getValue(),
meta: { breakpoint: "xxs" },
},
{
accessorKey: "age",
header: "Age",
cell: (info: any) => info.getValue(),
meta: { breakpoint: "sm" },
},
];

const data: Person[] = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 20 },
];

describe("DataTable", () => {
beforeEach(() => {
bpSignal[1]({
xxs: true,
sm: true,
md: true,
});
});

it("renders table headers and rows", () => {
render(() => <DataTable id="people" columns={columns} data={data} />);

expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Age")).toBeInTheDocument();

expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
expect(screen.getByText("30")).toBeInTheDocument();
expect(screen.getByText("20")).toBeInTheDocument();
});

it("renders fallback when there is no data", () => {
render(() => (
<DataTable
id="empty"
columns={columns}
data={[]}
fallback={<div>No data</div>}
/>
));

expect(screen.getByText("No data")).toBeInTheDocument();
});

it("sorts rows when clicking a sortable header", async () => {
render(() => <DataTable id="sorting" columns={columns} data={data} />);

const ageHeaderButton = screen.getByRole("button", { name: "Age" });
const ageHeaderCell = ageHeaderButton.closest("th");

// Initial
expect(ageHeaderCell).toHaveAttribute("aria-sort", "none");
expect(ageHeaderCell?.querySelector("i")).toHaveClass("fa-fw");

// Descending
await fireEvent.click(ageHeaderButton);
expect(ageHeaderCell).toHaveAttribute("aria-sort", "descending");
expect(ageHeaderCell?.querySelector("i")).toHaveClass(
"fa-sort-down",
"fas",
"fa-fw",
);
expect(localStorage()).toEqual([
{
desc: true,
id: "age",
},
]);

let rows = screen.getAllByRole("row");
expect(rows[1]).toHaveTextContent("Alice"); // age 30
expect(rows[2]).toHaveTextContent("Bob"); // age 20

// Ascending
await fireEvent.click(ageHeaderButton);
expect(ageHeaderCell).toHaveAttribute("aria-sort", "ascending");
expect(ageHeaderCell?.querySelector("i")).toHaveClass(
"fa-sort-up",
"fas",
"fa-fw",
);
expect(localStorage()).toEqual([
{
desc: false,
id: "age",
},
]);

rows = screen.getAllByRole("row");
expect(rows[1]).toHaveTextContent("Bob");
expect(rows[2]).toHaveTextContent("Alice");

//back to initial
await fireEvent.click(ageHeaderButton);
expect(ageHeaderCell).toHaveAttribute("aria-sort", "none");
expect(localStorage()).toEqual([]);
});

it("hides columns based on breakpoint visibility", () => {
bpSignal[1]({
xxs: true,
sm: false,
md: false,
});

render(() => <DataTable id="breakpoints" columns={columns} data={data} />);

expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.queryByText("Age")).not.toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@sentry/browser": "9.14.0",
"@sentry/vite-plugin": "3.3.1",
"@solidjs/meta": "0.29.4",
"@tanstack/solid-table": "8.21.3",
"@ts-rest/core": "3.52.1",
"animejs": "4.2.2",
"balloon-css": "1.2.0",
Expand All @@ -49,7 +50,7 @@
"idb": "8.0.3",
"konami": "1.7.0",
"lz-ts": "1.1.2",
"modern-screenshot": "4.6.5",
"modern-screenshot": "4.6.8",
"object-hash": "3.0.0",
"slim-select": "2.9.2",
"stemmer": "2.0.1",
Expand Down
1 change: 0 additions & 1 deletion frontend/src/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,5 @@ <h1 class="text">
<load src="html/pages/404.html" />
</main>
</div>
<link rel="stylesheet" href="/styles/standalone.scss" />
</body>
</html>
1 change: 0 additions & 1 deletion frontend/src/email-handler.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="styles/index.scss" />
<meta name="name" content="Monkeytype" />
<meta name="image" content="https://monkeytype.com/mtsocial.png" />
<meta
Expand Down
1 change: 0 additions & 1 deletion frontend/src/privacy-policy.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<link rel="stylesheet" href="styles/standalone.scss" />
<link id="favicon" rel="shortcut icon" href="images/fav.png" />
<link rel="shortcut icon" href="images/fav.png" />
<link rel="stylesheet" href="styles/index.scss" />
<meta name="name" content="Monkeytype" />
<meta name="image" content="https://monkeytype.com/mtsocial.png" />
<meta
Expand Down
1 change: 0 additions & 1 deletion frontend/src/security-policy.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<link rel="stylesheet" href="styles/standalone.scss" />
<link id="favicon" rel="shortcut icon" href="images/fav.png" />
<link rel="shortcut icon" href="images/fav.png" />
<link rel="stylesheet" href="styles/index.scss" />
<meta name="name" content="Monkeytype" />
<meta name="image" content="https://monkeytype.com/mtsocial.png" />
<meta
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/styles/standalone.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
@import "balloon-css/balloon.min.css";
@import "core.scss";

@font-face {
font-family: Roboto Mono;
font-style: normal;
font-weight: 400;
font-display: block;
src: url(/webfonts/RobotoMono-Regular.woff2) format("woff2");
}
@font-face {
font-family: Lexend Deca;
font-style: normal;
font-weight: 400;
font-display: block;
src: url(/webfonts/LexendDeca-Regular.woff2) format("woff2");
}

:root {
--font: "Roboto Mono", monospace;
--bg-color: #323437;
--main-color: #e2b714;
--caret-color: #e2b714;
Expand All @@ -11,3 +29,70 @@
--colorful-error-color: #ca4754;
--colorful-error-extra-color: #7e2a33;
}

header {
grid-template-areas: "logo menu";
line-height: 2.3rem;
font-size: 2.3rem;
/* text-align: center; */
// transition: 0.25s;
// padding: 0 5px;
display: grid;
grid-auto-flow: column;
grid-template-columns: auto 1fr;
z-index: 3;
align-items: center;
gap: 0.5rem;
-webkit-user-select: none;
user-select: none;

#logo {
// margin-bottom: 0.6rem;
cursor: pointer;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem;
transition: none;
text-decoration: none;
color: var(--text-color);
padding: 0.35rem 0.25rem;
margin-left: -0.25rem;
margin-right: -0.25rem;

.icon {
width: 2.5rem;
display: grid;
align-items: center;
background-color: transparent;
// margin-bottom: 0.15rem;
svg path {
transition: 0.25s;
fill: var(--main-color);
}
}
.text {
.top {
position: absolute;
left: 0.35em;
font-size: 0.325em;
line-height: 0.325em;
color: var(--sub-color);
transition:
color 0.125s,
opacity 0.125s;
}
position: relative;
font-size: 2rem;
line-height: 2rem;
font-family: "Lexend Deca", sans-serif;
transition: color 0.25s;
font-weight: unset;
margin-block-start: unset;
margin-block-end: unset;
margin-top: -0.23em;
}
white-space: nowrap;
-webkit-user-select: none;
user-select: none;
}
}
3 changes: 3 additions & 0 deletions frontend/src/styles/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@
.rounded-half {
border-radius: calc(var(--roundness) / 2);
}
.has-button\:p-0:has(button) {
padding: 0;
}
}
1 change: 0 additions & 1 deletion frontend/src/terms-of-service.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<link rel="stylesheet" href="styles/standalone.scss" />
<link id="favicon" rel="shortcut icon" href="images/fav.png" />
<link rel="shortcut icon" href="images/fav.png" />
<link rel="stylesheet" href="styles/index.scss" />
<meta name="name" content="Monkeytype" />
<meta name="image" content="https://monkeytype.com/mtsocial.png" />
<meta
Expand Down
Loading
Loading