diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9224c7..f7b9752 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,18 @@ on: jobs: test: - name: Test and Lint + name: Test and Build runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24.x + - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: @@ -26,8 +31,11 @@ jobs: - name: Lint code run: bun run lint - - name: Build userscript (test) + - name: Run tests + run: bun run test + + - name: Build userscript run: bun run build:userscript - - name: Build apps (test) + - name: Build apps run: bun run build:apps diff --git a/.gitignore b/.gitignore index 443b409..896a6ec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist/ .astro/ .wrangler/ + +.env diff --git a/apps/icbe/README.md b/apps/icbe/README.md new file mode 100644 index 0000000..e69de29 diff --git a/apps/icbe/astro.config.ts b/apps/icbe/astro.config.ts new file mode 100644 index 0000000..d5e5041 --- /dev/null +++ b/apps/icbe/astro.config.ts @@ -0,0 +1,29 @@ +import cloudflare from "@astrojs/cloudflare"; +import solid from "@astrojs/solid-js"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig, envField } from "astro/config"; + +export default defineConfig({ + output: "server", + adapter: cloudflare(), + integrations: [solid()], + vite: { plugins: [tailwindcss()] }, + site: "https://icbe.rman.dev", + + env: { + schema: { + GITLAB_ACCESS_TOKEN: envField.string({ + context: "server", + access: "secret", + startsWith: "glpat", + length: 26, + }), + GITLAB_PROJECT_ID: envField.number({ + context: "server", + access: "secret", + int: true, + gt: 1, + }), + }, + }, +}); diff --git a/apps/icbe/package.json b/apps/icbe/package.json index 46e648f..7a1753f 100644 --- a/apps/icbe/package.json +++ b/apps/icbe/package.json @@ -2,13 +2,28 @@ "name": "web-app", "type": "module", "private": true, + "imports": { + "#styles": "./src/styles/global.css", + "#layout": "./src/layouts/Layout.astro", + "#schemas": "./src/schemas/index.ts", + "#utils/*": "./src/utils/*.ts", + "#components/*": "./src/components/*.tsx" + }, "scripts": { + "typegen": "wrangler types ./src/worker-configuration.d.ts", + "test": "bun test", "build": "astro build", "deploy": "wrangler deploy", "dev": "astro dev" }, "dependencies": { - "astro": "catalog:" + "@astrojs/cloudflare": "^13.1.3", + "@astrojs/solid-js": "^6.0.1", + "@gitbeaker/rest": "^43.8.0", + "@tailwindcss/vite": "catalog:", + "astro": "catalog:", + "solid-js": "^1.9.11", + "tailwindcss": "catalog:" }, "devDependencies": { "wrangler": "catalog:" diff --git a/apps/icbe/public/.assetsignore b/apps/icbe/public/.assetsignore new file mode 100644 index 0000000..b7ccb6c --- /dev/null +++ b/apps/icbe/public/.assetsignore @@ -0,0 +1,2 @@ +_worker.js +_routes.json diff --git a/apps/icbe/src/actions/index.ts b/apps/icbe/src/actions/index.ts new file mode 100644 index 0000000..18801f5 --- /dev/null +++ b/apps/icbe/src/actions/index.ts @@ -0,0 +1,8 @@ +import { + submitDisprovenElements, + submitProvenElements, +} from "./submit-elements"; + +export const server = { + elements: { submitDisprovenElements, submitProvenElements }, +}; diff --git a/apps/icbe/src/actions/submit-elements.ts b/apps/icbe/src/actions/submit-elements.ts new file mode 100644 index 0000000..81c0a83 --- /dev/null +++ b/apps/icbe/src/actions/submit-elements.ts @@ -0,0 +1,65 @@ +import { ActionError, defineAction } from "astro:actions"; +import * as z from "astro/zod"; +import { DataProvenSchema } from "#schemas"; +import { saveToCloud } from "#utils/gitlab"; +import { getDisprovenToSave, getProvenToSave } from "#utils/submit-elements"; + +const submitDisprovenElements = defineAction({ + input: z.object({ elements: z.array(z.object({ name: z.string() })) }), + async handler(input, context) { + const role = null; + + if (!role) { + throw new ActionError({ + code: "UNAUTHORIZED", + message: "Please login to use this feature.", + }); + } + + const canSubmitElements = false; + + if (!canSubmitElements) { + throw new ActionError({ + code: "FORBIDDEN", + message: "You don't have permissions to use this feature.", + }); + } + + const elements = input.elements.map((element) => element.name); + + const data = await getDisprovenToSave(elements); + await saveToCloud(data); + + return { success: true } as const; + }, +}); + +const submitProvenElements = defineAction({ + input: z.object({ elements: DataProvenSchema }), + handler: async (input, context) => { + const role = null; + + if (!role) { + throw new ActionError({ + code: "UNAUTHORIZED", + message: "Please login to use this feature.", + }); + } + + const canSubmitElements = false; + + if (!canSubmitElements) { + throw new ActionError({ + code: "FORBIDDEN", + message: "You don't have permissions to use this feature.", + }); + } + + const data = await getProvenToSave(input.elements); + await saveToCloud(data); + + return { success: true } as const; + }, +}); + +export { submitDisprovenElements, submitProvenElements }; diff --git a/apps/icbe/src/components/AdminDashboard.tsx b/apps/icbe/src/components/AdminDashboard.tsx new file mode 100644 index 0000000..ac2a318 --- /dev/null +++ b/apps/icbe/src/components/AdminDashboard.tsx @@ -0,0 +1,11 @@ +import type { JSX } from "solid-js"; + +export default function AdminDashboard(): JSX.Element { + return ( +
+

+ Admin Controls +

+
+ ); +} diff --git a/apps/icbe/src/components/UserDashboard.tsx b/apps/icbe/src/components/UserDashboard.tsx new file mode 100644 index 0000000..946a347 --- /dev/null +++ b/apps/icbe/src/components/UserDashboard.tsx @@ -0,0 +1,40 @@ +import { type JSX, Show } from "solid-js"; + +import SubmitDisprovenElementsSection from "./dashboard/SubmitDisprovenElementsSection"; +import SubmitProvenElementsSection from "./dashboard/SubmitProvenElementsSection"; +import Divider from "./ui/Divider"; +import Link from "./ui/Link"; + +const TopBar = (): JSX.Element => ( +
+

+ User Dashboard +

+
+); + +export default function UserDashboard(props: { + canSubmitElements: boolean; +}): JSX.Element { + const isAdmin = false; + + return ( +
+ + + + + + + + + + + +
+ ); +} diff --git a/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx b/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx new file mode 100644 index 0000000..52dd14e --- /dev/null +++ b/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx @@ -0,0 +1,97 @@ +import { actions } from "astro:actions"; +import type { JSX } from "solid-js"; +import { createSignal, Show } from "solid-js"; +import Button from "#components/ui/Button"; + +type SubmitState = + | { status: "not-started" } + | { status: "submitting" } + | { status: "complete"; success: boolean } + | { status: "error"; error?: string }; + +const SubmitDisprovenElementsResultContainer = (props: { + state: SubmitState; +}) => ( + + {(state) => ( +

+ {state.success + ? "Elements submitted successfully!" + : "Elements submission failed."} +

+ )} +
+); + +function SubmitDisprovenElementsSection(): JSX.Element { + const [elementsToBeSubmitted, setElementsToBeSubmitted] = createSignal< + string[] + >([]); + const [submitState, setSubmitState] = createSignal({ + status: "not-started", + }); + + const handleSubmit = async () => { + try { + setSubmitState({ status: "submitting" }); + const disprovenElements = elementsToBeSubmitted().map((name) => ({ + name, + })); + const result = await actions.elements.submitDisprovenElements({ + elements: disprovenElements, + }); + if (result.error) { + console.error("Submit failed:", result.error); + setSubmitState({ status: "error", error: result.error.message }); + } else { + setSubmitState({ status: "complete", success: result.data.success }); + } + } catch (err) { + console.error("Submit error:", err); + setSubmitState({ status: "error" }); + } + }; + + return ( +
+