Repository: ucl-arc-tre/portal
Pull request #64: added basic starter layout, pages, and components
Author: login: jhughes982
Labels: name: enhancement
description: New feature or request
name: UI
description: User interface
Created: 2025-05-12T13:06:14Z | Merged: 2025-05-13T09:02:28Z

## Resolves #63 

- This PR adds a basic app layout so that we have a template to follow for creating different levels of components (i.e. pages, feature logic, UI etc.)
- A few dummy pages have been added, along with a basic form,
- No thought has been put into styling for now as this can be discussed separate to this code implementation,
- If anyone has any strong opinions, we can discuss and decide on the template structure to follow here.
- This PR is deliberately basic for now, and no logic has been hooked up to any backend, I wanted to keep each stage simple so we can discuss along the way.

I also made a few decisions in this PR that can dictate our style going forward. Let me know if you disagree with any of the following:
- Pages and components have their own associated `.css` file,
- Opted to not use css modules (it complicates the logic and I've worked on large codebases with plain css and it's worked fine),
- Pages have a central `page.tsx` entrypoint and feature components are linked within them (e.g. see the `approved-researcher` folder),
- Components that are specifically linked to a page can live inside a components folder at the same directory level as the `page.tsx` file.
- Components or logic that are shared across multiple files can live in the `components` folder at the `/src/components` location.



### Checklist

- [ ] Documentation has been updated


Linked issue #63: Create basic App layout
- To get things started, we should add a few basic pages to the portal so that we can kick-off conversations about the app flow.

## Definition of done
- [x] A basic layout is added to the NextJs setup with links to a few dummy pages
- [x] A page containing a basic form
- [x] Start a structure/template to follow for creating pages, page components, and lower-level UI components

Diff:
diff --git a/web/package-lock.json b/web/package-lock.json
index 6e19f4db9..afe9a4233 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -6303,6 +6303,96 @@
       "peerDependencies": {
         "zod": "^3.24.1"
       }
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz",
+      "integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz",
+      "integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz",
+      "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz",
+      "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz",
+      "integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "15.3.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz",
+      "integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==",
+      "cpu": [
+        "x64"
+      ],
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
     }
   }
 }
diff --git a/web/src/app/favicon.ico b/web/public/favicon.ico
similarity index 100%
rename from web/src/app/favicon.ico
rename to web/public/favicon.ico
diff --git a/web/public/next.svg b/web/public/next.svg
deleted file mode 100644
index 5bb00d403..000000000
--- a/web/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx
deleted file mode 100644
index a108e01b9..000000000
--- a/web/src/app/dashboard/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function Dashboard() {
-  const outstandingRequests = 0;
-
-  return (
-    <div>
-      {outstandingRequests > 0 && <p>Outstanding requests: {outstandingRequests}</p>}
-      <div>
-        <button>Studies</button>
-        <button>Projects</button>
-        <button>Assets</button>
-      </div>
-    </div>
-  );
-}
diff --git a/web/src/app/layout.css b/web/src/app/layout.css
new file mode 100644
index 000000000..08bbf13cc
--- /dev/null
+++ b/web/src/app/layout.css
@@ -0,0 +1,10 @@
+.layout {
+  display: flex;
+  min-height: 100vh;
+  font-family: sans-serif;
+}
+
+.main-content {
+  flex: 1;
+  padding: 2rem;
+}
diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx
index b14207400..cae6cbb5c 100644
--- a/web/src/app/layout.tsx
+++ b/web/src/app/layout.tsx
@@ -1,22 +1,22 @@
-import type { Metadata } from "next";
-
 import "./globals.css";
-import "./ui/buttons.css";
+import "./layout.css";
+
+import Nav from "@/components/nav/Nav";
+import type { ReactNode } from "react";
 
-export const metadata: Metadata = {
+export const metadata = {
   title: "UCL ARC | portal",
   description: "Portal for UCL ARC services",
 };
 
-export default function RootLayout({
-  children,
-}: Readonly<{
-  children: React.ReactNode;
-}>) {
+export default function RootLayout({ children }: { children: ReactNode }) {
   return (
     <html lang="en">
       <body>
-        <main className="main-content">{children}</main>
+        <div className="layout">
+          <Nav />
+          <main className="main-content">{children}</main>
+        </div>
       </body>
     </html>
   );
diff --git a/web/src/app/page.module.css b/web/src/app/page.css
similarity index 100%
rename from web/src/app/page.module.css
rename to web/src/app/page.css
diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
index 4df8317e3..3b0950d0e 100644
--- a/web/src/app/page.tsx
+++ b/web/src/app/page.tsx
@@ -2,15 +2,9 @@
 
 import { useEffect, useState } from "react";
 
-import styles from "./page.module.css";
-import Link from "next/link";
+import "./page.css";
 
 import { getProfile } from "@/openapi";
-import { client } from "@/openapi/client.gen";
-
-client.setConfig({
-  baseUrl: "/api/v0",
-});
 
 export default function Home() {
   const [helloMessage, setHelloMessage] = useState<string>("");
@@ -38,7 +32,7 @@ export default function Home() {
   }, []);
 
   return (
-    <div className={styles.page}>
+    <div className="page">
       <h1 id="title--portal">Welcome to the ARC Services Portal</h1>
 
       {isAuthed === undefined && <p>Loading...</p>}
@@ -55,10 +49,8 @@ export default function Home() {
             <p id="confirmation--login">
               GET /profile → <strong>{helloMessage}</strong>
             </p>
-
-            <Link href="/dashboard">
-              <button>Go to Dashboard</button>
-            </Link>
+            <div>Your tasks:</div>
+            <div>List of user tasks here (e.g. approved researcher process)</div>
           </div>
         </>
       )}
diff --git a/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.css b/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.css
new file mode 100644
index 000000000..fe076f9bf
--- /dev/null
+++ b/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.css
@@ -0,0 +1,41 @@
+.form-container {
+  max-width: 400px;
+  margin: 2rem auto;
+  padding: 1rem;
+}
+
+.form-title {
+  font-size: 1.5rem;
+  font-weight: bold;
+  margin-bottom: 1rem;
+}
+
+.input-field {
+  width: 100%;
+  padding: 0.5rem;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  margin-bottom: 1rem;
+}
+
+.submit-button {
+  width: 100%;
+  padding: 0.5rem;
+  background-color: #2563eb;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.submit-button:hover {
+  background-color: #1d4ed8;
+}
+
+.submitted-box {
+  margin-top: 1rem;
+  padding: 0.5rem;
+  background-color: #171717;
+  border-radius: 4px;
+  font-size: 0.9rem;
+}
diff --git a/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.tsx b/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.tsx
new file mode 100644
index 000000000..0aee3db51
--- /dev/null
+++ b/web/src/app/profile/approved-researcher/components/ApprovedResearcherForm.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
+  });
+
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setFormData((prev) => ({ ...prev, [name]: value }));
+  };
+
+  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    console.log("Form submitted:", formData);
+    setSubmitted(true);
+  };
+
+  return (
+    <div className="form-container">
+      <h1 className="form-title">User Info Form</h1>
+
+      <form onSubmit={handleSubmit}>
+        <input
+          type="text"
+          name="name"
+          placeholder="Name"
+          value={formData.name}
+          onChange={handleChange}
+          className="input-field"
+          required
+        />
+
+        <input
+          type="email"
+          name="email"
+          placeholder="Email"
+          value={formData.email}
+          onChange={handleChange}
+          className="input-field"
+          required
+        />
+
+        <input
+          type="text"
+          name="userID"
+          placeholder="User ID"
+          value={formData.userID}
+          onChange={handleChange}
+          className="input-field"
+          required
+        />
+
+        <input
+          type="text"
+          name="department"
+          placeholder="Department"
+          value={formData.department}
+          onChange={handleChange}
+          className="input-field"
+          required
+        />
+
+        <button type="submit" className="submit-button">
+          Submit
+        </button>
+      </form>
+
+      {submitted && (
+        <div className="submitted-box">
+          <p>Form submitted! Here`&apos;`s the data:</p>
+
+          <pre>{JSON.stringify(formData, null, 2)}</pre>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/web/src/app/profile/approved-researcher/page.tsx b/web/src/app/profile/approved-researcher/page.tsx
new file mode 100644
index 000000000..b29af6d1b
--- /dev/null
+++ b/web/src/app/profile/approved-researcher/page.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { getProfile } from "@/openapi";
+
+import ApprovedResearcherForm from "./components/ApprovedResearcherForm";
+
+export default function ProfilePage() {
+  const [isAuthed, setIsAuthed] = useState<boolean | undefined>(undefined);
+  const [username, setUsername] = useState<string | null>(null);
+
+  useEffect(() => {
+    const checkAuth = async () => {
+      try {
+        const response = await getProfile();
+
+        if (response.response.status === 200 && response.data) {
+          setIsAuthed(true);
+          setUsername(response.data.username);
+        } else {
+          setIsAuthed(false);
+        }
+      } catch (err) {
+        console.error("Profile fetch error:", err);
+        setIsAuthed(false);
+      }
+    };
+
+    checkAuth();
+  }, []);
+
+  if (isAuthed === undefined) return <p>Loading...</p>;
+
+  if (!isAuthed) {
+    return (
+      <div className="profile-page">
+        <h1>Access Profile</h1>
+
+        <p>You must be logged in to view this page</p>
+
+        <a href="/oauth2/start" className="btn--login" id="login" role="button">
+          Login with UCL SSO
+        </a>
+      </div>
+    );
+  }
+
+  return (
+    <div className="profile-page">
+      <h1>Welcome, {username}!</h1>
+
+      <ApprovedResearcherForm />
+    </div>
+  );
+}
diff --git a/web/src/app/profile/page.tsx b/web/src/app/profile/page.tsx
new file mode 100644
index 000000000..9e1b12426
--- /dev/null
+++ b/web/src/app/profile/page.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+
+import { getProfile, ProfileResponse } from "@/openapi";
+
+import "./profile.css";
+
+export default function ProfilePage() {
+  const [isAuthed, setIsAuthed] = useState<boolean | undefined>(undefined);
+  const [userData, setUserData] = useState<ProfileResponse | null>(null);
+
+  useEffect(() => {
+    const checkAuth = async () => {
+      try {
+        const response = await getProfile();
+
+        if (response.response.status === 200 && response.data) {
+          setIsAuthed(true);
+          setUserData(response.data);
+        } else {
+          setIsAuthed(false);
+        }
+      } catch (err) {
+        console.error("Profile fetch error:", err);
+        setIsAuthed(false);
+      }
+    };
+
+    checkAuth();
+  }, []);
+
+  if (isAuthed === undefined) return <p>Loading...</p>;
+
+  if (!isAuthed) {
+    return (
+      <div className="profile-page">
+        <h1>Access Profile</h1>
+
+        <p>You must be logged in to view this page</p>
+
+        <a href="/oauth2/start" className="btn--login" id="login" role="button">
+          Login with UCL SSO
+        </a>
+      </div>
+    );
+  }
+
+  return (
+    <div className="profile-page">
+      <h1>Welcome, {userData?.username}!</h1>
+      <p>This is your profile. More profile features coming soon.</p>
+
+      {!userData?.roles?.includes("approved-researcher") && (
+        <p>
+          It looks like you are not yet an approved researcher. To get started, follow the link below to fill out the
+          form:
+          <Link href="/profile/approved-researcher" className="btn--form">
+            Approved Researcher Form
+          </Link>
+          .
+        </p>
+      )}
+    </div>
+  );
+}
diff --git a/web/src/app/profile/profile.css b/web/src/app/profile/profile.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/src/app/studies/page.tsx b/web/src/app/studies/page.tsx
new file mode 100644
index 000000000..39d461c3c
--- /dev/null
+++ b/web/src/app/studies/page.tsx
@@ -0,0 +1,3 @@
+export default function Studies() {
+  return <div>Studies page</div>;
+}
diff --git a/web/src/components/nav/Nav.css b/web/src/components/nav/Nav.css
new file mode 100644
index 000000000..1022b4308
--- /dev/null
+++ b/web/src/components/nav/Nav.css
@@ -0,0 +1,30 @@
+.sidebar {
+  width: 220px;
+  background-color: var(--background);
+  padding: 1rem;
+}
+
+.sidebar-title {
+  font-size: 1.25rem;
+  font-weight: bold;
+  margin-bottom: 1rem;
+}
+
+.nav-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.nav-list li {
+  margin: 0.5rem 0;
+}
+
+.nav-list a {
+  text-decoration: none;
+  color: var(--foreground);
+}
+
+.nav-list a:hover {
+  text-decoration: underline;
+}
diff --git a/web/src/components/nav/Nav.tsx b/web/src/components/nav/Nav.tsx
new file mode 100644
index 000000000..7fc7260ec
--- /dev/null
+++ b/web/src/components/nav/Nav.tsx
@@ -0,0 +1,38 @@
+"use client";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import "./Nav.css";
+
+export default function Nav() {
+  const pathname = usePathname();
+
+  return (
+    <aside className="sidebar">
+      <h2 className="sidebar-title">UCL ARC TRE</h2>
+
+      <nav>
+        <ul className="nav-list">
+          <li className={pathname === "/" ? "active" : ""}>
+            <Link href="/">Home</Link>
+          </li>
+
+          <li className={pathname.startsWith("/assets") ? "active" : ""}>
+            <Link href="/assets">Assets</Link>
+          </li>
+
+          <li className={pathname.startsWith("/studies") ? "active" : ""}>
+            <Link href="/studies">Studies</Link>
+          </li>
+
+          <li className={pathname.startsWith("/projects") ? "active" : ""}>
+            <Link href="/projects">Projects</Link>
+          </li>
+
+          <li className={pathname.startsWith("/profile") ? "active" : ""}>
+            <Link href="/profile">Profile</Link>
+          </li>
+        </ul>
+      </nav>
+    </aside>
+  );
+}
diff --git a/web/src/app/ui/buttons.css b/web/src/components/ui/Button.css
similarity index 71%
rename from web/src/app/ui/buttons.css
rename to web/src/components/ui/Button.css
index 7cfc2efca..4b5dd8695 100644
--- a/web/src/app/ui/buttons.css
+++ b/web/src/components/ui/Button.css
@@ -1,4 +1,4 @@
-.btn--login {
+.button--login {
   font-size: 1.6rem;
   padding: 0.4rem;
 }
diff --git a/web/src/components/ui/Button.tsx b/web/src/components/ui/Button.tsx
new file mode 100644
index 000000000..040005118
--- /dev/null
+++ b/web/src/components/ui/Button.tsx
@@ -0,0 +1,27 @@
+import "./Button.css";
+
+type Props = React.ComponentProps<"button"> & {
+  inverse?: boolean;
+  danger?: boolean;
+  size?: string;
+  type?: string;
+  name?: string;
+  value?: string;
+  onClick?: React.MouseEventHandler<HTMLButtonElement>;
+};
+
+export default function Button(props: Props) {
+  return (
+    <button
+      className={`button ${`button--${props.size}`} ${props.inverse && "button--inverse"}
+      }`}
+      type={props.type}
+      onClick={props.onClick}
+      disabled={props.disabled}
+      name={props.name}
+      value={props.value}
+    >
+      {props.children}
+    </button>
+  );
+}


Reviews:
user: login: t-young31
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:24:59Z
user: login: acholyn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:48:49Z
user: login: t-young31
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:52:53Z
user: login: acholyn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:56:50Z
user: login: jhughes982
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:57:34Z
user: login: acholyn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T14:24:25Z
user: login: jhughes982
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:25:01Z
user: login: t-young31
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:46:12Z
user: login: jhughes982
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:48:23Z
user: login: t-young31
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:59:46Z
user: login: t-young31
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T07:46:07Z
user: login: jhughes982
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T08:49:32Z
user: login: jhughes982
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T08:53:13Z
user: login: t-young31
body: 🚀
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T08:59:56Z

Review comments:
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: t-young31
body: don't think we need any of this 😄
created_at: 2025-05-12T13:24:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: acholyn
body: do we need their EntraID? and we said we wanted a display name they can control, but not sure if that's this form
created_at: 2025-05-12T13:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: t-young31
body: the backend already has their EntraID. agree the display name should probably be before this form – probably be prompted on first access?


created_at: 2025-05-12T13:52:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: acholyn
body: so we probably only need one from the email and entra ID and this can be hidden from the user but can still be part of the form to link the doc to them. Unless the way we store the docs means they're already linked? (eg `docs/<entraID>/approved-researcher`)
created_at: 2025-05-12T13:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: jhughes982
body: Ah ok, I took the fields from [this thing called 'Approved Researcher form'](https://liveuclac.sharepoint.com/sites/ARCTrustedResearch-ProdSharePoint/_layouts/15/listforms.aspx?cid=YTc4YTdmMTctMjJlOC00OTAyLTg1NDktMmRmOWVkMmQ3ZGI0&nav=NjIyZmQzNGYtOGEyMC00YzdlLTgzYmMtYTkxM2MwNTRjZmRj) 😅
created_at: 2025-05-12T13:57:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: acholyn
body: probably don't need the id on this form since the agreed docs will be under the user
created_at: 2025-05-12T14:24:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+"use client";
+
+import { useState } from "react";
+
+import "./ApprovedResearcherForm.css";
+
+export default function ApprovedResearcherForm() {
+  const [submitted, setSubmitted] = useState(false);
+  const [formData, setFormData] = useState({
+    name: "",
+    email: "",
+    userID: "",
+    department: "",
path: web/src/app/approved-researcher/components/ApprovedResearcherForm.tsx
user: login: jhughes982
body: As this PR is not really about completing the approved researcher process, I'd like to focus on that in a follow-up PR to keep this strictly about setting up a basic layout. Unless anyone objects?
created_at: 2025-05-12T16:25:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,3 @@
+import { client } from "@/openapi/client.gen";
+
+client.setConfig({ baseUrl: "/api/v0" });
path: web/src/lib/setupOpenApi.ts
user: login: t-young31
body: don't think we need this anymore as it's now codegen-ed correctly https://github.com/ucl-arc-tre/portal/blob/f6d677f64fa7f9028ebad08b7a06ee078f6356c5/web/src/openapi/client.gen.ts#L16-L18
created_at: 2025-05-12T16:46:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,3 @@
+import { client } from "@/openapi/client.gen";
+
+client.setConfig({ baseUrl: "/api/v0" });
path: web/src/lib/setupOpenApi.ts
user: login: jhughes982
body: Nice! I'll remove it.
created_at: 2025-05-12T16:48:23Z
author_association: CONTRIBUTOR
path: .vscode/settings.json
user: login: t-young31
body: I'd rather ignore vscode settings
created_at: 2025-05-12T16:59:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { getProfile } from "@/openapi";
+import { client } from "@/openapi/client.gen";
+
+import ApprovedResearcherForm from "./components/ApprovedResearcherForm";
+
+client.setConfig({
+  baseUrl: "/api/v0",
+});
path: web/src/app/profile/approved-researcher/page.tsx
user: login: t-young31
body: ```suggestion
```
created_at: 2025-05-12T17:38:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { getProfile } from "@/openapi";
+import { client } from "@/openapi/client.gen";
+
+import ApprovedResearcherForm from "./components/ApprovedResearcherForm";
+
+client.setConfig({
+  baseUrl: "/api/v0",
+});
+
+export default function ProfilePage() {
+  const [isAuthed, setIsAuthed] = useState<boolean | undefined>(undefined);
+  const [username, setUsername] = useState<string | null>(null);
+
+  useEffect(() => {
+    const checkAuth = async () => {
+      try {
+        const response = await getProfile();
+
+        if (response.response.status === 200 && response.data) {
+          setIsAuthed(true);
+          setUsername(response.data.username);
+        } else {
+          setIsAuthed(false);
+        }
+      } catch (err) {
+        console.error("Profile fetch error:", err);
+        setIsAuthed(false);
+      }
+    };
+
+    checkAuth();
path: web/src/app/profile/approved-researcher/page.tsx
user: login: t-young31
body: for my own understanding: why define then call?
created_at: 2025-05-12T17:39:20Z
author_association: MEMBER
path: .vscode/settings.json
user: login: jhughes982
body: Ah, sneaky vscode put that in automatically. I'll remove it.
created_at: 2025-05-13T08:49:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,60 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { getProfile } from "@/openapi";
+import { client } from "@/openapi/client.gen";
+
+import ApprovedResearcherForm from "./components/ApprovedResearcherForm";
+
+client.setConfig({
+  baseUrl: "/api/v0",
+});
+
+export default function ProfilePage() {
+  const [isAuthed, setIsAuthed] = useState<boolean | undefined>(undefined);
+  const [username, setUsername] = useState<string | null>(null);
+
+  useEffect(() => {
+    const checkAuth = async () => {
+      try {
+        const response = await getProfile();
+
+        if (response.response.status === 200 && response.data) {
+          setIsAuthed(true);
+          setUsername(response.data.username);
+        } else {
+          setIsAuthed(false);
+        }
+      } catch (err) {
+        console.error("Profile fetch error:", err);
+        setIsAuthed(false);
+      }
+    };
+
+    checkAuth();
path: web/src/app/profile/approved-researcher/page.tsx
user: login: jhughes982
body: It's because we can't make a `useEffect` itself `async`. So in order to use async operations we must define an async function inside the `useEffect`.
created_at: 2025-05-13T08:53:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: jhughes982
created_at: 2025-05-12T13:13:25Z
body: Accessibility test is failing, I'll fix that after lunch unless someone has any time today?
author_association: CONTRIBUTOR
user: login: jhughes982
created_at: 2025-05-12T13:56:05Z
body: @acholyn the favicon needs to go in the `/app` directory. I favour suggesting changes to people's PR's rather than pushing directly to them (unless it's been agreed), otherwise we risk getting entangled in people making changes that may get out of sync.
author_association: CONTRIBUTOR
user: login: acholyn
created_at: 2025-05-12T13:59:50Z
body: should still be in the /app directory, I only removed the next.svg and put in our own icon so the folder wouldn't disappear
author_association: CONTRIBUTOR
user: login: jhughes982
created_at: 2025-05-12T15:15:53Z
body: > should still be in the /app directory, I only removed the next.svg and put in our own icon so the folder wouldn't disappear

Sounds good, still if you could suggest it as a change rather than a direct push that would be preferable 🙂

edit: It looks like you have duplicated the file, as it now exists in both `/app` and in the `public` folder. It should be put in one or the other folder, not both.
author_association: CONTRIBUTOR
user: login: jhughes982
created_at: 2025-05-12T16:50:00Z
body: The basics have all been set up here now. I'd like to iterate on this in follow-up PR's unless there are further refinements people would like here?
author_association: CONTRIBUTOR