(resp);
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/api/tests/embed.test.ts:
--------------------------------------------------------------------------------
1 | import type { Document, Note } from "../types";
2 |
3 | import { test, describe, expect } from "vitest";
4 |
5 | import { BASE_API_URL } from "@/config/config.js";
6 | import { embedUrl } from "../embed";
7 |
8 | import * as documents from "../documents";
9 | import * as notes from "../notes.js";
10 |
11 | import document from "@/test/fixtures/documents/document.json";
12 | import note from "@/test/fixtures/notes/note.json";
13 |
14 | describe("embed tests", () => {
15 | test("embedUrl", () => {
16 | // document
17 | const docUrl = documents.canonicalUrl(document as Document);
18 |
19 | expect(embedUrl(docUrl)).toStrictEqual(
20 | new URL(
21 | `oembed/?url=${encodeURIComponent(docUrl.toString())}`,
22 | BASE_API_URL,
23 | ),
24 | );
25 |
26 | // note
27 | const noteUrl = notes.noteUrl(document as Document, note as Note);
28 | expect(embedUrl(noteUrl)).toStrictEqual(
29 | new URL(
30 | `oembed/?url=${encodeURIComponent(noteUrl.toString())}`,
31 | BASE_API_URL,
32 | ),
33 | );
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/lib/api/tests/flatpages.test.ts:
--------------------------------------------------------------------------------
1 | import { afterEach, describe, expect, test, vi } from "vitest";
2 |
3 | import { BASE_API_URL } from "@/config/config";
4 | import { flatpage } from "@/test/fixtures/flatpages";
5 | import * as flatpages from "../flatpages";
6 |
7 | describe("flatpages", () => {
8 | afterEach(() => {
9 | vi.restoreAllMocks();
10 | });
11 |
12 | test("flatpages.get", async () => {
13 | const mockFetch = vi.fn().mockImplementation(async (endpoint, options) => {
14 | return {
15 | ok: true,
16 | status: 200,
17 | async json() {
18 | return flatpage;
19 | },
20 | };
21 | });
22 |
23 | const { error, data } = await flatpages.get(flatpage.url, mockFetch);
24 |
25 | expect(error).toBeUndefined();
26 | expect(data).toEqual(flatpage);
27 |
28 | expect(mockFetch).toHaveBeenCalledWith(
29 | new URL("/api/flatpages" + flatpage.url, BASE_API_URL),
30 | {
31 | credentials: "include",
32 | },
33 | );
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/Avatar.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {#if user}
11 | {#if user.avatar_url}
12 |

13 | {:else}
14 |
15 | {/if}
16 | {:else if org}
17 | {#if org.avatar_url}
18 |

19 | {:else}
20 |
21 | {/if}
22 | {/if}
23 |
24 |
25 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/stories/Avatar.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
29 |
30 |
31 |
40 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/stories/Mailkey.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/stories/Unverified.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/tests/Mailkey.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import Mailkey from "../Mailkey.svelte";
4 |
5 | test("Mailkey", () => {
6 | let result = render(Mailkey);
7 | expect(result.container).toMatchSnapshot();
8 | result = render(Mailkey, {message: 'Something happened'});
9 | expect(result.container).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/src/lib/components/accounts/tests/UserMenu.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import { me as meFixture } from "@/test/fixtures/accounts";
4 | import UserMenu from "../UserMenu.svelte";
5 |
6 | test("UserMenu", async () => {
7 | let result = render(UserMenu, {user: meFixture});
8 | expect(result.container).toMatchSnapshot();
9 | });
10 |
--------------------------------------------------------------------------------
/src/lib/components/addons/stories/AddOnMeta.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
17 |
18 |
19 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/addons/stories/AddOnsNavigation.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/addons/stories/History.stories.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
34 |
39 |
44 |
--------------------------------------------------------------------------------
/src/lib/components/addons/stories/Scheduled.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/addons/stories/ScheduledEvent.stories.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/addons/tests/AddOnListItem.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 |
4 | import AddOnListItem from "../AddOnListItem.svelte";
5 | import { addon, premiumAddon } from "@/test/fixtures/addons";
6 |
7 | test("AddOnListItem", () => {
8 | const result = render(AddOnListItem, { addon });
9 | expect(result.getByRole("heading").textContent).toEqual(addon.name);
10 | expect(result.container).toMatchSnapshot();
11 | const premium = render(AddOnListItem, { addon: premiumAddon });
12 | expect(premium.getByRole("status").textContent).toContain("Premium");
13 | expect(premium.container).toMatchSnapshot();
14 | });
15 |
--------------------------------------------------------------------------------
/src/lib/components/addons/tests/AddOnsNavigation.test.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import AddOnsNavigation from "../AddOnsNavigation.svelte";
4 |
5 | test("AddOnsNavigation", () => {
6 | const result = render(AddOnsNavigation);
7 | expect(result.container).toMatchSnapshot();
8 | });
9 |
--------------------------------------------------------------------------------
/src/lib/components/common/Badge.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 | {#if $$slots.icon}{/if}
14 | {label}
15 |
16 |
17 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/common/Card.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/lib/components/common/Copy.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/common/Empty.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | {#if icon}{/if}
9 |
10 |
11 |
12 |
28 |
--------------------------------------------------------------------------------
/src/lib/components/common/Error.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/common/Field.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
19 |
20 | {#if $$slots.help}
{/if}
21 |
22 |
23 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/common/Menu.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
8 |
9 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/common/MenuInsert.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/common/Metadata.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/common/PageToolbar.svelte:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
27 |
28 |
54 |
--------------------------------------------------------------------------------
/src/lib/components/common/PlausibleTracker.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/components/common/Premium.svelte:
--------------------------------------------------------------------------------
1 |
6 |
20 |
21 | {#if isPremium}
22 |
23 | {:else}
24 |
25 | {/if}
26 |
--------------------------------------------------------------------------------
/src/lib/components/common/ShowSize.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | {#if size >= 1 && size <= MAX_EDIT_BATCH}
8 |
9 |
10 | {:else if size > MAX_EDIT_BATCH}
11 |
12 |
13 | {:else}
14 |
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/lib/components/common/SignedIn.svelte:
--------------------------------------------------------------------------------
1 |
6 |
11 |
12 | {#if isSignedIn($me)}
13 |
14 | {:else}
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Action.stories.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | Open
16 |
17 |
18 |
19 | Edit
20 |
21 |
22 |
23 | Edit
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Badge.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Empty.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | Nothing to see here!
15 |
16 |
17 |
18 | No results found for search "FYSA"
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Error.stories.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | Something has gone terribly wrong!
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/FieldLabel.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | Input Label
21 |
22 |
23 |
24 |
25 | Never-Ending Salad & Breadsticks
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/HighlightGroup.stories.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/KV.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
48 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Logo.stories.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Metadata.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | March 21, 2024
17 |
18 |
19 |
20 |
21 | March 21, 2024
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/PageToolbar.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 | Left
18 | Center
19 | Right
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Pin.stories.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/RelativeTime.stories.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/common/stories/Tooltip.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/common/tests/Button.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from "vitest";
2 | import { render, screen } from "@testing-library/svelte";
3 |
4 | import Button from "../Button.svelte";
5 |
6 | describe("Button", () => {
7 | it("renders a default button", () => {
8 | render(Button);
9 |
10 | const button = screen.getByRole("button");
11 |
12 | expect(button).toBeInTheDocument();
13 | expect(button.tagName).toStrictEqual("BUTTON");
14 | });
15 |
16 | it("renders a link when href is used", () => {
17 | render(Button, { props: { href: "https://www.documentcloud.org" } });
18 |
19 | const link = screen.getByText(/Submit/);
20 |
21 | expect(link).toBeInTheDocument();
22 | expect(link.tagName).toStrictEqual("A");
23 | });
24 |
25 | it("sets name and value properties in button mode", () => {
26 | render(Button, { props: { name: "action", value: "add" } });
27 |
28 | const button = screen.getByRole("button");
29 |
30 | expect(button).toBeInTheDocument();
31 | expect(button.getAttribute("name")).toEqual("action");
32 | expect(button.getAttribute("value")).toEqual("add");
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/lib/components/common/tests/ShowSize.demo.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {size} items
11 | Zero items
12 | Too many items!
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/common/tests/ShowSize.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import ShowSizeDemo from "./ShowSize.demo.svelte";
4 | import { MAX_EDIT_BATCH } from "@/config/config.js";
5 |
6 | describe("ShowSize.svelte", () => {
7 | it("renders default slot when size is within valid range", () => {
8 | const { getByText } = render(ShowSizeDemo, {
9 | props: {
10 | size: 5,
11 | },
12 | });
13 |
14 | expect(getByText("5 items")).toBeTruthy();
15 | });
16 |
17 | it("renders oversize slot when size exceeds MAX_EDIT_BATCH", () => {
18 | const { getByText } = render(ShowSizeDemo, {
19 | props: {
20 | size: MAX_EDIT_BATCH + 1,
21 | },
22 | });
23 | expect(getByText("Too many items!")).toBeTruthy();
24 | });
25 |
26 | it("renders empty slot when size is 0", () => {
27 | const { getByText } = render(ShowSizeDemo, {
28 | props: {
29 | size: 0,
30 | },
31 | });
32 | expect(getByText("Zero items")).toBeTruthy();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/lib/components/common/tests/Toast.test.ts:
--------------------------------------------------------------------------------
1 | import { vi, describe, it, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import { userEvent } from "@testing-library/user-event";
4 | import Toast from "../Toast.svelte";
5 |
6 | describe("Toast", () => {
7 | it("calls close when user clicks close button", async () => {
8 | const user = userEvent.setup();
9 | const container = render(Toast, { lifespan: 1000 });
10 | // Spy on the "close" event
11 | const closeSpy = vi.fn();
12 | container.component.$on("close", closeSpy);
13 | // Click the close button
14 | const closeButton = container.getByRole("button");
15 | await user.click(closeButton);
16 | // Check if the "close" event was dispatched
17 | expect(closeSpy).toHaveBeenCalledTimes(1);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/lib/components/documents/Pending.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 | {#if pending.length > 0}
12 |
19 |
20 | {$_("processingBar.processing")}
21 |
22 | {$_("processingBar.processingDocuments", {
23 | values: { n: pending.length },
24 | })}
25 |
26 |
27 | {/if}
28 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/CustomizeEmbed.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {createEmbedSearchParams($embedSettings)}
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/Metadata.stories.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/NoteHighlights.stories.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/PageHighlights.stories.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/Pending.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/Projects.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/Revisions.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/documents/stories/Thumbnail.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/components/documents/tests/DocumentListItem.test.ts:
--------------------------------------------------------------------------------
1 | import { vi, describe, it, beforeEach, expect } from "vitest";
2 | import { render } from "@testing-library/svelte";
3 | import { document } from "@/test/fixtures/documents";
4 | import DocumentListItem from "../DocumentListItem.svelte";
5 |
6 | describe("DocumentListItem", async () => {
7 | beforeEach(() => {
8 | // Mock Date.now() to return a fixed timestamp
9 | vi.spyOn(Date, "now").mockReturnValue(1620000000000); // Mocked timestamp
10 | });
11 | it("renders", () => {
12 | let result = render(DocumentListItem, { document });
13 | expect(result.container).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/lib/components/embeds/stories/DocumentEmbed.stories.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
43 |
--------------------------------------------------------------------------------
/src/lib/components/forms/DeleteProject.svelte:
--------------------------------------------------------------------------------
1 |
4 |
22 |
23 |
36 |
37 |
45 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/ConfirmRedaction.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/DeleteProject.stories.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/EditData.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/EditNote.stories.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/EditProject.stories.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/InviteCollaborator.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/Projects.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/RemoveCollaborator.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/Reprocess.stories.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | {$_("dialogReprocessDialog.title")}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {$_("dialogReprocessDialog.title")}
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/RevisionControl.stories.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | action("change")} />
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/Sections.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/UpdateCollaborator.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/forms/stories/UserFeedback.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/icons/Premium.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
20 |
21 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/Choices.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/Language.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/generator.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentType } from "svelte";
2 |
3 | import ArrayField from "./ArrayField.svelte";
4 | import Checkbox from "./Checkbox.svelte";
5 | import Number from "./Number.svelte";
6 | import Text from "./Text.svelte";
7 | import Choices from "./Choices.svelte";
8 |
9 | // https://json-schema.org/understanding-json-schema/reference/type.html
10 | const fields = {
11 | string: Text,
12 | number: Number,
13 | integer: Number,
14 | boolean: Checkbox,
15 | null: null,
16 |
17 | // compound fields, tbd
18 | array: ArrayField,
19 | object: null,
20 | };
21 |
22 | /**
23 | * Automatically get a form field component based on the property type of a JSON schema object
24 | *
25 | */
26 | export function autofield(params: any, fallback = Text): ComponentType {
27 | const type = String(params.type).toLowerCase();
28 |
29 | if (!fields[type]) {
30 | console.warn("Unknown field type: %s", type);
31 | return fallback;
32 | }
33 |
34 | // only string enums for now
35 | if (type === "string" && params.enum) {
36 | return Choices;
37 | }
38 |
39 | return fields[type];
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/AccessLevel.stories.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Checkbox.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/File.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | Select Files
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/KeyValue.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Language.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/ProjectAccess.stories.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Select.stories.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Switch.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Text.stories.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
32 |
33 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/stories/Textarea.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
34 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/AppLayout.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
49 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/Portal.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/SidebarLayout.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
25 |
26 |
51 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/stories/AddOnBrowser.stories.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
36 |
41 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/stories/DocumentBrowser.stories.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/stories/Error.stories.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 | Something broke on our end!
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Page not found
24 |
25 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/layouts/stories/Flatpage.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/navigation/stories/LanguageMenu.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/premium-credits/PremiumBadge.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/premium-credits/Price.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 | {value?.toLocaleString()}
11 |
12 |
13 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/premium-credits/stories/PremiumBadge.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/premium-credits/stories/Price.stories.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/src/lib/components/premium-credits/stories/UpgradePrompt.stories.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/processing/AddOns.svelte:
--------------------------------------------------------------------------------
1 |
5 |
20 |
21 | {#if $running?.length && $running.length > 0}
22 |
23 |
24 |
25 | {$_("processing.addons")}
26 |
27 |
28 | {#each $running as run (run.uuid)}
29 |
30 | {/each}
31 |
32 | {/if}
33 |
--------------------------------------------------------------------------------
/src/lib/components/processing/stories/AddOns.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/processing/stories/Documents.stories.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/lib/components/processing/stories/ProcessDropdown.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/components/processing/stories/ProcessSummary.stories.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/projects/stories/Collaborators.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/lib/components/projects/stories/ProjectHeader.stories.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/UploadButton.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 | {#if isSignedIn($me) && canUploadFiles($me)}
30 | {#if project}
31 |
39 | {:else}
40 |
43 | {/if}
44 | {/if}
45 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/stories/AddOns.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
26 | {}) }} />
27 |
28 |
36 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/stories/DocumentActions.stories.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/stories/Documents.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/stories/ProjectActions.stories.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar/stories/ViewerActions.stories.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/AnnotationToolbar.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | {$_("annotate.title")}
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
45 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/LoadingToolbar.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
Loading PDF…
11 | {#if progress}
12 |
13 | {:else}
14 |
15 | {/if}
16 |
17 |
18 |
19 |
35 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/NoteTab.svelte:
--------------------------------------------------------------------------------
1 |
7 |
14 |
15 |
16 |
17 |
46 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/Grid.stories.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/LoadingToolbar.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/NoteTab.stories.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/Notes.stories.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/Section.stories.svelte:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/src/lib/components/viewer/stories/Section.stories.svelte
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/Text.stories.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/lib/components/viewer/stories/Zoom.stories.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/lib/i18n/index.js:
--------------------------------------------------------------------------------
1 | import { browser } from "$app/environment";
2 | import { init, register } from "svelte-i18n";
3 | import { LANGUAGES } from "@/config/config.js";
4 | import { getLanguage } from "../utils/language";
5 |
6 | const defaultLocale = "en";
7 |
8 | LANGUAGES.forEach(([name, code, flag]) => {
9 | if (code) {
10 | register(code, () => import(`@/langs/json/${code}.json`));
11 | }
12 | });
13 |
14 | // handle two-part locales
15 | register("en-US", () => import("@/langs/json/en.json"));
16 | register("en-GB", () => import("@/langs/json/en.json"));
17 |
18 | init({
19 | fallbackLocale: defaultLocale,
20 | initialLocale: browser
21 | ? getLanguage(window.navigator?.language ?? defaultLocale)
22 | : defaultLocale,
23 | });
24 |
--------------------------------------------------------------------------------
/src/lib/utils/array.ts:
--------------------------------------------------------------------------------
1 | export function includes(
2 | array: Array,
3 | elem: T,
4 | eq = (a?: T, b?: T) => a == b,
5 | ) {
6 | for (let i = 0; i < array.length; i++) {
7 | if (eq(array[i], elem)) return true;
8 | }
9 | return false;
10 | }
11 |
12 | export function intersection(
13 | arrays: Array>,
14 | eq = (a: T, b: T) => a == b,
15 | ) {
16 | if (arrays.length == 0) return [];
17 | // Adapted from https://stackoverflow.com/a/59176460
18 | return arrays.reduce((a, b) => a.filter((elem) => includes(b, elem, eq)));
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/utils/copy.ts:
--------------------------------------------------------------------------------
1 | import { unwrapFunctionStore, _ } from "svelte-i18n";
2 | import { toast } from "$lib/components/layouts/Toaster.svelte";
3 |
4 | export default async function copy(text: string) {
5 | const $_ = unwrapFunctionStore(_);
6 | try {
7 | await navigator.clipboard.writeText(text);
8 | toast($_("share.copiedToClipboard"));
9 | } catch {
10 | toast($_("share.couldNotCopyToClipboard"), { status: "error" });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./api";
2 | export { breadcrumbTrail } from "./navigation";
3 |
--------------------------------------------------------------------------------
/src/lib/utils/language.ts:
--------------------------------------------------------------------------------
1 | // handle language parsing here so we can test it
2 |
3 | export function getLanguage(code: string, fallback: string = "en") {
4 | const [language, ...tags] = code.split("-");
5 | return language || fallback;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/utils/layout.ts:
--------------------------------------------------------------------------------
1 | const REM_BASE = 16;
2 |
3 | /** Calcuates the px value for a provided rem amount. */
4 | export function remToPx(num: number): number {
5 | return REM_BASE * num;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/utils/logging.ts:
--------------------------------------------------------------------------------
1 | // log requests
2 |
3 | import type { RequestEvent } from "@sveltejs/kit";
4 |
5 | /**
6 | * Log a request inside a Handle function
7 | * @param status
8 | * @param event
9 | */
10 | export function log(event: RequestEvent, response: Response): void {
11 | // be loud about errors
12 | const status = response.status;
13 | const f = status >= 400 ? console.warn : console.info;
14 |
15 | const { method, url } = event.request;
16 |
17 | const headers = Object.fromEntries(
18 | // @ts-ignore
19 | [...response.headers].filter(([k, v]) => k !== "link"),
20 | );
21 |
22 | const row = {
23 | timestamp: new Date().toJSON(),
24 | method,
25 | url,
26 | status,
27 | route: event.route.id,
28 | headers,
29 | };
30 |
31 | f(JSON.stringify(row));
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/utils/markdown.ts:
--------------------------------------------------------------------------------
1 | import { marked } from "marked";
2 | import { gfmHeadingId } from "marked-gfm-heading-id";
3 | import DOMPurify from "isomorphic-dompurify";
4 |
5 | marked.use(gfmHeadingId());
6 |
7 | export function renderMarkdown(content: string) {
8 | return DOMPurify.sanitize(marked.parse(content));
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/utils/navigation.ts:
--------------------------------------------------------------------------------
1 | type Breadcrumb = { href?: string; title: string };
2 | type Parent = () => Promise<{ breadcrumbs?: Array }>;
3 |
4 | /** Returns a trail of breadcrumbs, built upon the parent's trail.
5 | * Primarily for use in `+layout.ts` and `+page.ts` files.
6 | */
7 | export async function breadcrumbTrail(
8 | parent: Parent,
9 | crumbs: Array = [],
10 | ) {
11 | const { breadcrumbs: trail = [] } = await parent();
12 | return trail.concat(crumbs);
13 | }
14 |
15 | /**
16 | * Make a link preserve existing query params and merge in new ones
17 | *
18 | * @type {Action}
19 | * @param {HTMLAnchorElement} node
20 | * @param {boolean} enable
21 | */
22 | export function qs(node: HTMLAnchorElement, enable = true): void {
23 | if (typeof window === "undefined") return;
24 | if (!enable) return;
25 |
26 | const href = new URL(node.href);
27 | const params = new URLSearchParams(window.location.search);
28 |
29 | for (const [k, v] of href.searchParams) {
30 | params.set(k, v);
31 | }
32 |
33 | href.search = params.toString();
34 | node.href = href.toString();
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/utils/permissions.ts:
--------------------------------------------------------------------------------
1 | /** Checks whether users have permission to view, update, or delete resources. */
2 |
3 | import type { Writable } from "svelte/store";
4 | import type { Nullable, User } from "$lib/api/types";
5 |
6 | import { getContext } from "svelte";
7 |
8 | /**
9 | * A helper that returns the signed-in user.
10 | * @returns the signed in user from context.
11 | * @throws if called outside component initialization.
12 | */
13 | export function getCurrentUser(): Writable {
14 | return getContext("me");
15 | }
16 |
17 | /** Checks if the provided user exists. If they do, they are signed in. */
18 | export function isSignedIn(user?: User | null): user is User {
19 | return Boolean(user);
20 | }
21 |
22 | /* Checks if the user can upload file. Must be verified journalist or staff. */
23 | export function canUploadFiles(user?: Nullable): boolean {
24 | if (!user) return false;
25 | return Boolean(user.verified_journalist || user.is_staff);
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/utils/scroll.ts:
--------------------------------------------------------------------------------
1 | import { pageHashUrl } from "../api/documents";
2 |
3 | /**
4 | * Scroll to a page using a standard ID
5 | *
6 | * @param n page number
7 | */
8 | export function scrollToPage(n: number): void {
9 | const pageId = pageHashUrl(n).replace("#", "");
10 | const heading = window.document.getElementById(pageId);
11 |
12 | if (!heading) return console.warn(`Missing page ${n}`);
13 | heading.scrollIntoView();
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/utils/slugify.ts:
--------------------------------------------------------------------------------
1 | export function slugify(str: string, id = null, maxLength = 25) {
2 | const slug = str
3 | .substring(0, maxLength)
4 | .toLowerCase()
5 | .replace(/\s+/g, "-")
6 | .replace(/[^a-z0-9-]/g, "");
7 | if (id != null) return `${id}-${slug}`;
8 | return slug;
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/utils/tests/language.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from "vitest";
2 | import { getLanguage } from "../language";
3 |
4 | // code, expected
5 | const languages = [
6 | ["en", "en"],
7 | ["en-US", "en"],
8 | ["en-GB", "en"],
9 | ["es", "es"],
10 | ["--", "en"],
11 | ];
12 |
13 | describe("language", () => {
14 | test("split language codes", () => {
15 | for (const [code, expected] of languages) {
16 | expect(getLanguage(code as string)).toEqual(expected);
17 | }
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/lib/utils/tests/pageSize.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 | import { pageSizesFromSpec, pageSizes } from "../pageSize";
3 |
4 | describe("pageSizesFromSpec", () => {
5 | test("page sizes empty", () => {
6 | expect(pageSizesFromSpec("")).toEqual([]);
7 | });
8 |
9 | test("page sizes simple", () => {
10 | expect(pageSizesFromSpec("1x1:0")).toEqual([1]);
11 | });
12 |
13 | test("page sizes complex", () => {
14 | expect(pageSizesFromSpec("1x1:1,3-5,0;1x2:2")).toEqual([1, 1, 2, 1, 1, 1]);
15 | });
16 | });
17 |
18 | describe("pageSizes", () => {
19 | test("page sizes empty", () => {
20 | expect(pageSizes("")).toEqual([]);
21 | });
22 |
23 | test("page sizes simple", () => {
24 | expect(pageSizes("1x1:0")).toEqual([[1, 1]]);
25 | });
26 |
27 | test("page sizes complex", () => {
28 | expect(pageSizes("1x1:1,3-5,0;1x2:2")).toEqual([
29 | [1, 1],
30 | [1, 1],
31 | [1, 2],
32 | [1, 1],
33 | [1, 1],
34 | [1, 1],
35 | ]);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/routes/(app)/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | export { load } from "sveltekit-flash-message/server";
2 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/routes/(app)/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { Actions } from "./$types";
2 |
3 | import { _ } from "svelte-i18n";
4 | import { fail } from "@sveltejs/kit";
5 | import { setFlash } from "sveltekit-flash-message/server";
6 |
7 | import { createFeedback, type Feedback } from "$lib/api/feedback";
8 |
9 | export const actions = {
10 | feedback: async ({ request, cookies, fetch }) => {
11 | const data = await request.formData();
12 | // POST form data to baserow
13 | const feedback: Feedback = {
14 | Type: String(data.get("type")) ?? "",
15 | Message: String(data.get("message")) ?? "",
16 | User: String(data.get("user")) ?? "",
17 | URL: String(data.get("url")) ?? "",
18 | };
19 | try {
20 | await createFeedback(feedback, fetch);
21 | setFlash(
22 | {
23 | message: "Feedback recieved, thanks for using DocumentCloud!",
24 | status: "success",
25 | },
26 | cookies,
27 | );
28 | return { success: true };
29 | } catch (e) {
30 | setFlash({ message: e.message, status: "error" }, cookies);
31 | fail(500, { message: String(e) });
32 | }
33 | },
34 | } satisfies Actions;
35 |
--------------------------------------------------------------------------------
/src/routes/(app)/+page.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "@sveltejs/kit";
2 | import { userDocs } from "$lib/utils/search";
3 |
4 | export const trailingSlash = "ignore";
5 |
6 | export async function load({ parent, url }) {
7 | const { me } = await parent();
8 | const u = new URL(url);
9 |
10 | if (me) {
11 | u.pathname = "/documents/";
12 | u.searchParams.set("q", userDocs(me));
13 | return redirect(307, u);
14 | }
15 |
16 | u.pathname = "/home/";
17 | return redirect(307, u);
18 | }
19 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/+layout.ts:
--------------------------------------------------------------------------------
1 | import { VIEWER_MAX_AGE } from "@/config/config.js";
2 | import { breadcrumbTrail } from "$lib/utils/navigation";
3 |
4 | export async function load({ parent, setHeaders }) {
5 | const { me } = await parent();
6 | const breadcrumbs = await breadcrumbTrail(parent, [
7 | { href: "/add-ons/", title: "Add-Ons" },
8 | ]);
9 |
10 | if (!me) {
11 | setHeaders({
12 | "cache-control": `public, max-age=${VIEWER_MAX_AGE}`,
13 | });
14 | }
15 |
16 | return {
17 | breadcrumbs,
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/+page.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 | Add-Ons | DocumentCloud
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/+page.ts:
--------------------------------------------------------------------------------
1 | import { getAddons, scheduled, history } from "$lib/api/addons";
2 |
3 | export async function load({ url, fetch }) {
4 | const params = Object.fromEntries(url.searchParams.entries());
5 | const addons = getAddons(params, fetch);
6 | const events = scheduled({ per_page: 5 }, fetch);
7 | const runs = history({ per_page: 5 }, fetch);
8 |
9 | return {
10 | addons,
11 | events,
12 | runs,
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/[owner]/[repo]/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/[owner]/[repo]/+page.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 | {addon.name} | Add-Ons | DocumentCloud
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/[owner]/[repo]/+page.ts:
--------------------------------------------------------------------------------
1 | import { error } from "@sveltejs/kit";
2 |
3 | import * as addons from "$lib/api/addons";
4 | import { search } from "$lib/api/documents";
5 | import { breadcrumbTrail } from "$lib/utils/navigation";
6 | import { userDocs } from "$lib/utils/search";
7 |
8 | export async function load({ url, params, fetch, parent }) {
9 | const { owner, repo } = params;
10 |
11 | const addon = await addons.getAddon(owner, repo, fetch);
12 | if (!addon) {
13 | return error(404, "Add-On not found");
14 | }
15 |
16 | const breadcrumbs = await breadcrumbTrail(parent, [
17 | { href: url.pathname, title: addon.name },
18 | ]);
19 |
20 | const { me } = await parent();
21 | const query = me ? url.searchParams.get("q") || userDocs(me) : "";
22 |
23 | const searchResults = search(query, {}, fetch).then((r) => r.data);
24 |
25 | return {
26 | addon,
27 | breadcrumbs,
28 | query,
29 | searchResults,
30 | scheduled: addons
31 | .scheduled({ addon: addon.id, per_page: 100 }, fetch)
32 | .then((r) => r.data),
33 | history: addons.history({ addon: addon.id }, fetch).then((r) => r.data),
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/routes/(app)/add-ons/runs/+page.svelte:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/src/routes/(app)/add-ons/runs/+page.svelte
--------------------------------------------------------------------------------
/src/routes/(app)/app/+page.ts:
--------------------------------------------------------------------------------
1 | // redirect /app to /
2 |
3 | import { redirect } from "@sveltejs/kit";
4 |
5 | export function load({ url }) {
6 | const u = new URL(url);
7 |
8 | // change the path but preserve other parts of the URL
9 | u.pathname = "/documents/";
10 |
11 | return redirect(308, u);
12 | }
13 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/+layout.ts:
--------------------------------------------------------------------------------
1 | import { breadcrumbTrail } from "$lib/utils/navigation";
2 |
3 | export async function load({ parent }) {
4 | const breadcrumbs = await breadcrumbTrail(parent, [
5 | { href: "/documents/", title: "Documents" },
6 | ]);
7 |
8 | return {
9 | breadcrumbs,
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/+page.ts:
--------------------------------------------------------------------------------
1 | import type { SearchOptions } from "$lib/api/types";
2 |
3 | import { DEFAULT_PER_PAGE, VIEWER_MAX_AGE } from "@/config/config.js";
4 | import { search } from "$lib/api/documents";
5 |
6 | export async function load({ url, fetch, data, parent, setHeaders }) {
7 | const query = url.searchParams.get("q") || "";
8 | const per_page = +(url.searchParams.get("per_page") ?? DEFAULT_PER_PAGE);
9 | const cursor = url.searchParams.get("cursor") || "";
10 |
11 | const options: SearchOptions = {
12 | hl: Boolean(query),
13 | version: "2.0",
14 | };
15 |
16 | if (per_page) {
17 | options.per_page = per_page;
18 | }
19 |
20 | if (cursor) {
21 | options.cursor = cursor;
22 | }
23 |
24 | const searchResults = search(query, options, fetch);
25 |
26 | const { me } = await parent();
27 |
28 | if (!me) {
29 | setHeaders({
30 | "cache-control": `public, max-age=${VIEWER_MAX_AGE}`,
31 | });
32 | }
33 |
34 | return {
35 | ...data,
36 | query,
37 | per_page,
38 | cursor,
39 | searchResults,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]-[slug]/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]-[slug]/annotations/[note_id]/+page.ts:
--------------------------------------------------------------------------------
1 | // this route was sometimes generated by old embeds and should redirect
2 |
3 | import { redirect } from "@sveltejs/kit";
4 | import { EMBED_URL } from "@/config/config.js";
5 |
6 | export function load({ params }) {
7 | const { id, note_id } = params;
8 |
9 | // the embed route can handle errors
10 | const url = new URL(
11 | `/documents/${id}/annotations/${note_id}/?embed=1`,
12 | EMBED_URL,
13 | );
14 |
15 | return redirect(302, url);
16 | }
17 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]-[slug]/pages/[page]/+page.ts:
--------------------------------------------------------------------------------
1 | // this route was sometimes generated by old embeds and should redirect
2 |
3 | import { redirect } from "@sveltejs/kit";
4 | import { EMBED_URL } from "@/config/config.js";
5 |
6 | export function load({ params }) {
7 | const { id, page } = params;
8 |
9 | // if this is a 404, the embed route will handle it
10 | const url = new URL(`/documents/${id}/pages/${page}/?embed=1`, EMBED_URL);
11 |
12 | return redirect(302, url);
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]/+page.ts:
--------------------------------------------------------------------------------
1 | // redirect /documents/id/ to /documents/id-slug/
2 | import { error, redirect } from "@sveltejs/kit";
3 | import * as documents from "$lib/api/documents";
4 |
5 | // save a redirect
6 | export const trailingSlash = "ignore";
7 |
8 | /** @type {import('./$types').PageLoad} */
9 | export async function load({ params, fetch, url }) {
10 | const { data: document, error: err } = await documents.get(+params.id, fetch);
11 | if (err) {
12 | console.warn(err.status, url.href);
13 | return error(err.status, err.message);
14 | }
15 |
16 | if (!document) {
17 | return error(404, "Document not found");
18 | }
19 | const canonical = documents.canonicalUrl(document);
20 |
21 | return redirect(308, canonical);
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]/annotations/[note_id]/+page.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "@sveltejs/kit";
2 |
3 | import { EMBED_URL } from "@/config/config.js";
4 |
5 | // redirect to embed route
6 | export function load({ url }) {
7 | const u = new URL(url);
8 |
9 | u.searchParams.set("embed", "1");
10 | u.hostname = EMBED_URL;
11 |
12 | return redirect(302, u);
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/(app)/documents/[id]/pages/[page]/+page.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "@sveltejs/kit";
2 |
3 | import { EMBED_URL } from "@/config/config.js";
4 |
5 | // redirect to embed route
6 | export function load({ url }) {
7 | const u = new URL(url);
8 |
9 | u.searchParams.set("embed", "1");
10 | u.hostname = EMBED_URL;
11 |
12 | return redirect(302, u);
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/(app)/projects/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/projects/+layout.ts:
--------------------------------------------------------------------------------
1 | import { breadcrumbTrail } from "$lib/utils/navigation";
2 |
3 | export async function load({ parent }) {
4 | const breadcrumbs = await breadcrumbTrail(parent, [
5 | { href: "/projects/", title: "Projects" },
6 | ]);
7 | return {
8 | breadcrumbs,
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/routes/(app)/projects/[id]-[slug]/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/projects/[id]/+page.ts:
--------------------------------------------------------------------------------
1 | // redirect to the full URL
2 |
3 | import { error, redirect } from "@sveltejs/kit";
4 | import * as projects from "$lib/api/projects";
5 |
6 | export async function load({ params, fetch }) {
7 | const { data: project, error: err } = await projects.get(+params.id, fetch);
8 |
9 | if (err) {
10 | return error(err.status, err.message);
11 | }
12 |
13 | if (!project) {
14 | return error(404, "Project not found");
15 | }
16 |
17 | const url = projects.canonicalUrl(project);
18 |
19 | return redirect(302, url);
20 | }
21 |
--------------------------------------------------------------------------------
/src/routes/(app)/upload/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(app)/upload/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | Upload | DocumentCloud
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
42 |
--------------------------------------------------------------------------------
/src/routes/(app)/upload/+page.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Data loading for upload
3 | */
4 | import { redirect } from "@sveltejs/kit";
5 | import * as projectsApi from "$lib/api/projects";
6 |
7 | export async function load({ fetch, parent }) {
8 | const { me } = await parent();
9 |
10 | if (!me) {
11 | return redirect(302, "/home/");
12 | }
13 |
14 | const projects = await projectsApi.list(
15 | { per_page: 100, user: me.id },
16 | fetch,
17 | );
18 |
19 | return {
20 | projects: projects.data,
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/(pages)/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/(pages)/about/+page.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | {title} | DocumentCloud
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/routes/(pages)/about/+page.ts:
--------------------------------------------------------------------------------
1 | import { error } from "@sveltejs/kit";
2 |
3 | import { PAGE_MAX_AGE } from "@/config/config.js";
4 | import * as flatpages from "$lib/api/flatpages";
5 |
6 | export const trailingSlash = "always";
7 |
8 | const path = "/about/";
9 |
10 | export async function load({ fetch, setHeaders }) {
11 | const { data, error: err } = await flatpages.get(path, fetch);
12 |
13 | if (err) {
14 | return error(err.status, { message: err.message });
15 | }
16 |
17 | if (!data) {
18 | return error(404, "Page not found");
19 | }
20 |
21 | setHeaders({
22 | "cache-control": `public, max-age=${PAGE_MAX_AGE}`,
23 | });
24 |
25 | return data;
26 | }
27 |
--------------------------------------------------------------------------------
/src/routes/(pages)/help/[...path]/+page.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {title} | DocumentCloud
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/routes/(pages)/help/[...path]/+page.ts:
--------------------------------------------------------------------------------
1 | // load data for flatpages
2 | import { error } from "@sveltejs/kit";
3 |
4 | import { PAGE_MAX_AGE } from "@/config/config.js";
5 | import * as flatpages from "$lib/api/flatpages";
6 |
7 | export const trailingSlash = "always";
8 |
9 | export async function load({ fetch, params, setHeaders }) {
10 | const path = ["help", ...params.path.split("/")].join("/");
11 | const { data, error: err } = await flatpages.get(path, fetch);
12 |
13 | if (err) {
14 | return error(err.status, { message: err.message });
15 | }
16 |
17 | if (!data) {
18 | return error(404, "Page not found");
19 | }
20 |
21 | setHeaders({
22 | "cache-control": `public, max-age=${PAGE_MAX_AGE}`,
23 | });
24 |
25 | return data;
26 | }
27 |
--------------------------------------------------------------------------------
/src/routes/(pages)/home/+page.ts:
--------------------------------------------------------------------------------
1 | // load homepage data
2 | import { error } from "@sveltejs/kit";
3 | import DOMPurify from "isomorphic-dompurify";
4 | import { marked } from "marked";
5 | import { gfmHeadingId } from "marked-gfm-heading-id";
6 |
7 | import { PAGE_MAX_AGE } from "@/config/config.js";
8 | import * as flatpages from "$lib/api/flatpages";
9 | import { getMe } from "$lib/api/accounts";
10 |
11 | marked.use(gfmHeadingId());
12 |
13 | export const trailingSlash = "ignore";
14 |
15 | export async function load({ fetch, setHeaders }) {
16 | const [{ data: page, error: err }, me] = await Promise.all([
17 | flatpages.get("/home/", fetch),
18 | getMe(fetch),
19 | ]);
20 |
21 | if (err) {
22 | return error(err.status, { message: err.message });
23 | }
24 |
25 | if (!page) {
26 | return error(404, "Page not found");
27 | }
28 |
29 | if (!me) {
30 | setHeaders({
31 | "cache-control": `public, max-age=${PAGE_MAX_AGE}`,
32 | });
33 | }
34 |
35 | return {
36 | title: page.title,
37 | url: page.url,
38 | content: render(page.content),
39 | me,
40 | };
41 | }
42 |
43 | function render(content: string): string {
44 | return DOMPurify.sanitize(marked.parse(content));
45 | }
46 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {#if $page.status === 404}
10 | {$_("notfound.content")}
11 | {:else}
12 | {$page.error?.message}
13 | {/if}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | import { browser } from "$app/environment";
2 | import { locale, waitLocale } from "svelte-i18n";
3 | import { getLanguage } from "$lib/utils/language";
4 |
5 | import "$lib/i18n/index.js"; // Import to initialize. Important :)
6 |
7 | export async function load() {
8 | if (browser) {
9 | let lang: string;
10 | try {
11 | lang = localStorage.getItem("dc-locale") || window.navigator.language;
12 | } catch {
13 | lang = window.navigator.language;
14 | }
15 |
16 | // use en.json for en-US and such
17 | const language = getLanguage(lang, "en");
18 |
19 | locale.set(language);
20 | }
21 | await waitLocale();
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/embed/+layout.ts:
--------------------------------------------------------------------------------
1 | export const trailingSlash = "ignore";
2 |
--------------------------------------------------------------------------------
/src/routes/embed/documents/[id]/+page.ts:
--------------------------------------------------------------------------------
1 | // redirect /documents/id/ to /documents/id-slug/
2 | import { error, redirect } from "@sveltejs/kit";
3 | import * as documents from "$lib/api/documents";
4 |
5 | // save a redirect
6 | export const trailingSlash = "ignore";
7 |
8 | /** @type {import('./$types').PageLoad} */
9 | export async function load({ params, fetch, url }) {
10 | const { data: document, error: err } = await documents.get(+params.id, fetch);
11 | if (err) {
12 | console.warn(err.status, url.href);
13 | return error(err.status, err.message);
14 | }
15 |
16 | if (!document) {
17 | return error(404, "Document not found");
18 | }
19 | const canonical = documents.embedUrl(document, url.searchParams);
20 |
21 | return redirect(308, canonical);
22 | }
23 |
--------------------------------------------------------------------------------
/src/routes/embed/documents/[id]/annotations/[note_id]/+page.ts:
--------------------------------------------------------------------------------
1 | // load a note for embedding
2 | import { error } from "@sveltejs/kit";
3 |
4 | import { EMBED_MAX_AGE } from "@/config/config.js";
5 | import * as documents from "$lib/api/documents";
6 | import * as notesApi from "$lib/api/notes";
7 |
8 | export async function load({ params, fetch, setHeaders }) {
9 | const [document, note] = await Promise.all([
10 | documents.get(+params.id, fetch),
11 | notesApi.get(+params.id, parseInt(params.note_id), fetch),
12 | ]);
13 |
14 | if (document.error || !document.data || note.error || !note.data) {
15 | return error(404, "Document not found");
16 | }
17 |
18 | setHeaders({
19 | "cache-control": `public, max-age=${EMBED_MAX_AGE}`,
20 | "last-modified": new Date(document.data.updated_at).toUTCString(),
21 | });
22 |
23 | return {
24 | document: document.data,
25 | note: note.data,
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/routes/embed/documents/[id]/pages/[page]/+page.ts:
--------------------------------------------------------------------------------
1 | // load data for a single page embed
2 | import { error } from "@sveltejs/kit";
3 |
4 | import { EMBED_MAX_AGE } from "@/config/config.js";
5 | import * as documents from "$lib/api/documents";
6 | import * as notesApi from "$lib/api/notes";
7 |
8 | /** @type {import('./$types').PageLoad} */
9 | export async function load({ params, fetch, setHeaders }) {
10 | const page = +params.page;
11 | let [document, notes] = await Promise.all([
12 | documents.get(+params.id, fetch),
13 | notesApi.list(+params.id, fetch),
14 | ]);
15 |
16 | if (document.error || !document.data) {
17 | return error(404, "Document not found");
18 | }
19 |
20 | setHeaders({
21 | "cache-control": `public, max-age=${EMBED_MAX_AGE}`,
22 | "last-modified": new Date(document.data.updated_at).toUTCString(),
23 | });
24 |
25 | return {
26 | document: document.data,
27 | notes:
28 | notes.data?.results.filter((note) => note.page_number === page - 1) ?? [],
29 | page: +page,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/style/README.md:
--------------------------------------------------------------------------------
1 | # Global styles
2 |
3 | - `kit.css` sets baseline typography and color ramps and defines global variables
4 | - `legacy.css` defines styles used by components in the previous version of the site (and will eventually go away)
5 | - `variables.css` defines variables used by older components (and will also go away)
6 |
--------------------------------------------------------------------------------
/src/test/components/UserContext.demo.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 | {$user?.name}
14 |
--------------------------------------------------------------------------------
/src/test/fixtures/common.ts:
--------------------------------------------------------------------------------
1 | import type { Page } from "$lib/api/types";
2 |
3 | export const emptyList: Page = {
4 | count: 0,
5 | next: null,
6 | previous: null,
7 | results: [],
8 | };
9 |
--------------------------------------------------------------------------------
/src/test/fixtures/documents/create.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 20000031,
4 | "access": "private",
5 | "admin_noindex": false,
6 | "asset_url": "https://api.dev.documentcloud.org/files/",
7 | "canonical_url": "https://www.dev.documentcloud.org/documents/20000031-finalseasonal-allergies-pollen-and-mold-2023-en",
8 | "created_at": "2024-04-15T01:22:45.191803Z",
9 | "data": {},
10 | "description": "",
11 | "edit_access": true,
12 | "file_hash": "",
13 | "noindex": false,
14 | "language": "eng",
15 | "organization": 10001,
16 | "original_extension": "pdf",
17 | "page_count": 0,
18 | "page_spec": "",
19 | "presigned_url": "https://documentcloud-dev-test.s3.amazonaws.com/documents/20000031/finalseasonal-allergies-pollen-and-mold-2023-en.pdf",
20 | "projects": [],
21 | "publish_at": null,
22 | "published_url": "",
23 | "related_article": "",
24 | "revision_control": false,
25 | "slug": "finalseasonal-allergies-pollen-and-mold-2023-en",
26 | "source": "",
27 | "status": "nofile",
28 | "title": "FINALSeasonal allergies pollen and mold 2023 EN",
29 | "updated_at": "2024-04-15T01:22:45.192143Z",
30 | "user": 100000
31 | }
32 | ]
33 |
--------------------------------------------------------------------------------
/src/test/fixtures/documents/examples/agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/src/test/fixtures/documents/examples/agreement-between-conservatives-and-liberal-democrats-to-form-a-coalition-government.pdf
--------------------------------------------------------------------------------
/src/test/fixtures/documents/examples/the-santa-anas.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/src/test/fixtures/documents/examples/the-santa-anas.pdf
--------------------------------------------------------------------------------
/src/test/fixtures/documents/pending.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "doc_id": 20000033,
4 | "images": 2,
5 | "texts": 2,
6 | "text_positions": 5,
7 | "pages": 11
8 | },
9 | {
10 | "doc_id": 20000067,
11 | "images": 131,
12 | "texts": 131,
13 | "text_positions": 141,
14 | "pages": 151
15 | },
16 | {
17 | "doc_id": 20000069,
18 | "images": 18,
19 | "texts": 18,
20 | "text_positions": 18,
21 | "pages": 151
22 | }
23 | ]
24 |
--------------------------------------------------------------------------------
/src/test/fixtures/documents/redactions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "page_number": 0,
4 | "x1": 0.88,
5 | "x2": 0.965,
6 | "y1": 0.16629955947136563,
7 | "y2": 0.3650708975770925
8 | },
9 | {
10 | "page_number": 1,
11 | "x1": 0.91,
12 | "x2": 0.945,
13 | "y1": 0.41409691629955947,
14 | "y2": 0.5655114262114538
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/src/test/fixtures/notes/note.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 557,
3 | "user": 126,
4 | "organization": 60,
5 | "page_number": 2,
6 | "access": "public",
7 | "edit_access": false,
8 | "title": "Setting a Date for the Next Election",
9 | "content": "Now we're getting to the heart of the document, and this is the most revolutionary proposal it contains. For the Liberal Democrats, nothing matters more than the need to reform the country's political system. Introducing fixed-term Parliaments, and even setting the date of the country's next election today, will end the age-old era in which the Prime Minister of the day could call an election on the most politically-advantageous date of his choosing. Parliament will still be able to pass a motion of \"no-confidence\" in the government, and - if 55% of lawmakers agree - the legislature would be dissolved prompting an early election. But Nick Clegg hopes this proposal is just the start of the seismic changes he wants to make to Britain's \"unwritten\" Constitution.",
10 | "x1": 0.09428571428571429,
11 | "x2": 0.6928571428571428,
12 | "y1": 0.5915676959619952,
13 | "y2": 0.6107482185273159,
14 | "created_at": "2010-05-12T16:54:26.655839Z",
15 | "updated_at": "2020-09-25T13:59:36.229576Z"
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/fixtures/oembed.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "provider_name": "DocumentCloud",
4 | "provider_url": "https://www.documentcloud.org",
5 | "cache_age": 300,
6 | "title": "Lefler Thesis",
7 | "width": 700,
8 | "height": 905,
9 | "html": "\n",
10 | "type": "rich"
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/fixtures/projects/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 215178,
3 | "created_at": "2023-09-21T14:36:10.910337Z",
4 | "description": "",
5 | "edit_access": false,
6 | "add_remove_access": false,
7 | "private": false,
8 | "slug": "ocr-reprise",
9 | "title": "OCR Reprise",
10 | "updated_at": "2023-09-21T14:36:10.911299Z",
11 | "user": 102112
12 | }
13 |
--------------------------------------------------------------------------------
/src/test/fixtures/sections.json:
--------------------------------------------------------------------------------
1 | {
2 | "next": null,
3 | "previous": null,
4 | "results": [
5 | {
6 | "id": 34582,
7 | "page_number": 2,
8 | "title": "This is a section"
9 | },
10 | {
11 | "id": 34583,
12 | "page_number": 5,
13 | "title": "Second section"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/handlers/feedback.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 | import {
3 | dataHandler,
4 | emptyHandler,
5 | loadingHandler,
6 | errorHandler,
7 | } from "./utils";
8 |
9 | const API_URL = "https://api.baserow.io/api/database/rows/table/*/*";
10 |
11 | export const feedback = {
12 | data: rest.post(API_URL, dataHandler({})),
13 | empty: rest.post(API_URL, emptyHandler()),
14 | loading: rest.post(API_URL, loadingHandler),
15 | error: rest.post(API_URL, errorHandler),
16 | };
17 |
--------------------------------------------------------------------------------
/src/test/handlers/oembed.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 |
3 | import oembedFixture from "../fixtures/oembed.json";
4 | import { createApiUrl, dataHandler } from "./utils";
5 |
6 | export const oembed = rest.get(
7 | createApiUrl("oembed/"),
8 | dataHandler(oembedFixture),
9 | );
10 |
--------------------------------------------------------------------------------
/src/test/handlers/projects.ts:
--------------------------------------------------------------------------------
1 | import type { Page } from "$lib/api/types";
2 |
3 | import { rest } from "msw";
4 |
5 | import {
6 | dataHandler,
7 | emptyHandler,
8 | errorHandler,
9 | loadingHandler,
10 | createApiUrl,
11 | pageHandler,
12 | } from "./utils";
13 | import { emptyList } from "../fixtures/common";
14 | import { projectList } from "../fixtures/projects";
15 | import projectFixture from "../fixtures/projects/project.json";
16 | import projDocsPage1 from "../fixtures/projects/project-documents-expanded.json";
17 | import projDocsPage2 from "../fixtures/projects/project-documents-2.json";
18 |
19 | const projectUrl = createApiUrl("projects/*");
20 |
21 | export const projects = {
22 | info: rest.get(createApiUrl("projects/"), dataHandler(projectList)),
23 | data: rest.get(projectUrl, dataHandler(projectFixture)),
24 | empty: rest.get(projectUrl, emptyHandler(emptyList)),
25 | loading: rest.get(projectUrl, loadingHandler),
26 | error: rest.get(projectUrl, errorHandler),
27 | documents: rest.get(
28 | createApiUrl("projects/*/documents/"),
29 | pageHandler }>>(
30 | projDocsPage1,
31 | projDocsPage2,
32 | ),
33 | ),
34 | };
35 |
--------------------------------------------------------------------------------
/src/test/handlers/viewer.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 |
3 | export const simulatePDF403Error = (asset_url: string) =>
4 | rest.get(asset_url, (req, res, ctx) =>
5 | res(ctx.status(403, "Not Found"), ctx.json({ detail: "Not found." })),
6 | );
7 |
--------------------------------------------------------------------------------
/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Not found
7 |
8 |
9 | Not found
10 |
11 |
12 |
--------------------------------------------------------------------------------
/static/_redirects:
--------------------------------------------------------------------------------
1 | # we don't have any of these
2 | /.well-known/* /404.html 404
3 | /ads.txt /404.html 404
4 |
5 | # sometimes asset paths get mislaid
6 | /documents/_app/* /_app/:splat
7 | /documents/:id/_app/* /_app/:splat
8 |
9 | # old URLs
10 | /search/ /documents/
11 | /app /documents/
12 | /api/oembed.json https://api.www.documentcloud.org/api/oembed/
13 |
--------------------------------------------------------------------------------
/static/apple-touch-icon-120x120-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/apple-touch-icon-120x120-precomposed.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-152x152-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/apple-touch-icon-152x152-precomposed.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/favicon.ico
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/favicon.png
--------------------------------------------------------------------------------
/static/fonts/SourceCodePro-Italic-VariableFont_wght.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/SourceCodePro-Italic-VariableFont_wght.ttf
--------------------------------------------------------------------------------
/static/fonts/SourceCodePro-VariableFont_wght.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/SourceCodePro-VariableFont_wght.ttf
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-600.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-600.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-700.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-700.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-italic.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-italic.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-menuset-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-menuset-600.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-menuset-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-menuset-600.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-regular.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-cyrillic-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-cyrillic-regular.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-latin-menuset-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-latin-menuset-600.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-latin-menuset-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-latin-menuset-600.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-600.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-600.eot
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-600.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-600.ttf
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-600.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-600.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-700.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-700.eot
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-700.ttf
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-700.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-700.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-italic.eot
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-italic.ttf
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-italic.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-italic.woff2
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-regular.eot
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-regular.ttf
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-regular.woff
--------------------------------------------------------------------------------
/static/fonts/source-sans-pro-v21-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/static/fonts/source-sans-pro-v21-latin-regular.woff2
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from "@sveltejs/adapter-netlify";
2 | import sveltePreprocess from "svelte-preprocess";
3 | import { fastDimension } from "svelte-fast-dimension";
4 | import autoprefixer from "autoprefixer";
5 |
6 | /** @type {import('@sveltejs/kit').Config} */
7 | export default {
8 | compilerOptions: {
9 | accessors: true,
10 | },
11 |
12 | kit: {
13 | adapter: adapter({
14 | preprocess: true,
15 | }),
16 | alias: {
17 | "@": "./src",
18 | "@/*": "./src/*",
19 | },
20 | csrf: {
21 | // BUG: https://github.com/sveltejs/kit/issues/8026
22 | checkOrigin: process.env.DOCKER === "true" ? false : true,
23 | },
24 | },
25 |
26 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
27 | // for more information about preprocessors
28 | preprocess: [
29 | fastDimension(),
30 | sveltePreprocess({
31 | postcss: {
32 | plugins: [autoprefixer],
33 | },
34 | typescript: {
35 | compilerOptions: {
36 | target: "es2020",
37 | },
38 | },
39 | }),
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # End-to-end tests
2 |
3 | This directory includes [Playwright](https://playwright.dev) tests that run in a headless browser against a running version of the site. To run locally, start a full instance (Squarelet, the DocumentCloud API and frontend) and set the `URL` environment variable. For example:
4 |
5 | ```sh
6 | URL=https://www.dev.documentcloud.org npx playwright test
7 | ```
8 |
9 | Github will automatically run this against any pull request using a deploy preview, and it will set the `URL` environment variable to the correct target.
10 |
--------------------------------------------------------------------------------
/tests/fixtures/Small pdf.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/tests/fixtures/Small pdf.pdf
--------------------------------------------------------------------------------
/tests/fixtures/the-nature-of-the-firm-CPEC11.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MuckRock/documentcloud-frontend/e1ec8fdbdd3b49aaa9a31aa405f68ea233289979/tests/fixtures/the-nature-of-the-firm-CPEC11.pdf
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "resolveJsonModule": true,
5 | "allowJs": true,
6 | "downlevelIteration": true,
7 | "noUncheckedIndexedAccess": true,
8 | "strictNullChecks": true,
9 | "types": ["vitest/globals", "@testing-library/jest-dom"]
10 | },
11 | "include": [
12 | "src/**/*.ts",
13 | "src/**/*.svelte",
14 | "src/**/*.js",
15 | "src/app.d.ts",
16 | ".svelte-kit/ambient.d.ts"
17 | ],
18 | "exclude": ["public/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | // "extends": "@tsconfig/svelte/tsconfig.json" /* all options: https://www.typescriptlang.org/tsconfig#resolveJsonModule */,
3 | "compilerOptions": {
4 | // "ignoreDeprecations": "5.0",
5 | // "verbatimModuleSyntax": true,
6 | "allowSyntheticDefaultImports": true,
7 | "resolveJsonModule": true,
8 | "lib": ["es2020", "DOM", "dom.iterable"],
9 | "allowJs": true,
10 | // "checkJs": true, /* 600+ errors */
11 | "baseUrl": "/app",
12 | "paths": {
13 | "@/*": ["src/*"],
14 | "svelte": ["node_modules/svelte"]
15 | },
16 | "downlevelIteration": true,
17 | "moduleResolution": "nodenext",
18 | "types": ["@testing-library/jest-dom"]
19 | },
20 | "include": [
21 | "src/**/*.ts",
22 | "src/**/*.svelte",
23 | "src/**/*.js",
24 | ".svelte-kit/ambient.d.ts"
25 | ],
26 | "exclude": ["public/*"]
27 | }
28 |
--------------------------------------------------------------------------------
/utility/README.md:
--------------------------------------------------------------------------------
1 | # Utility scripts
2 |
3 | ## Python scripts
4 |
5 | The `translate.py` script depends on the `uv` package to download dependencies (including the correct version of Python). It also needs a copy of Ollama running locally, using the `llama3.2` model.
6 |
7 | Run like this:
8 |
9 | ```sh
10 | ollama pull llama3.2
11 | ollama serve # if not already sarted
12 | uv run utilities/translate.py json src/langs/json/en.json -d src/langs/json/it.json -l Italian
13 | ```
14 |
15 | # Shell scripts
16 |
17 | Shell scripts are meant to run from the project root. Running from the `utility` folder will produce incorrect paths.
18 |
--------------------------------------------------------------------------------
/utility/find-all-assets.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ASSETS=$(find src/assets -name '*.svg' | xargs basename)
4 |
5 | for asset in $ASSETS; do
6 | ack --ignore-dir public --ignore-dir src/assets --ignore-dir scratch $asset
7 | done
8 |
--------------------------------------------------------------------------------
/utility/find-asset.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | asset=$(basename $1)
4 |
5 | ack --ignore-dir public --ignore-dir src/assets --ignore-dir scratch $asset
6 |
--------------------------------------------------------------------------------
/utility/find-unused-assets.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ASSETS=$(find src/assets -name '*.svg' | xargs basename)
4 |
5 | for asset in $ASSETS; do
6 | grep --quiet -r $asset src
7 | code=$?
8 | if [ 0 -ne $code ]; then
9 | echo $asset
10 | fi
11 | done
12 |
--------------------------------------------------------------------------------
/utility/purge.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | curl --request POST \
4 | --url "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ID}/purge_cache" \
5 | --header 'Content-Type: application/json' \
6 | --header "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \
7 | --data '{"purge_everything": true}'
8 |
--------------------------------------------------------------------------------
/vitest-setup.js:
--------------------------------------------------------------------------------
1 | import "@testing-library/svelte/vitest";
2 | import "@testing-library/jest-dom/vitest";
3 | import "./src/lib/i18n/index.js";
4 |
5 | import { vi } from "vitest";
6 | import ResizeObserver from "resize-observer-polyfill";
7 |
8 | vi.stubGlobal("ResizeObserver", ResizeObserver);
9 |
--------------------------------------------------------------------------------