Skip to content

Commit 61dc5fe

Browse files
authoredSep 5, 2024
Base UI work (#9)
* deps(ui): add tanstack dependencies : query, form, table * fix(ui): add dependencies and implement basic router * chore(ui): implement basic login page with working axios call * feat: implement create project page/form
1 parent df7b47a commit 61dc5fe

22 files changed

+2974
-446
lines changed
 

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ production.yml
6060
/notes/
6161
**/system.log
6262
/logs/
63+
64+
*.exe

‎internal/api/middleware.go

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func (api *API) middleware() {
1414

1515
app.Use(cors.New(cors.Config{
1616
AllowOrigins: "*", // TODO: use cors origins from configuration
17+
AllowMethods: "*",
1718
}))
1819
app.Use(pprof.New())
1920
app.Use(recover.New())

‎internal/api/v1/projects.go

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66

77
"github.com/gofiber/fiber/v2"
8+
"github.com/golang-malawi/qatarina/internal/api/authutil"
89
"github.com/golang-malawi/qatarina/internal/common"
910
"github.com/golang-malawi/qatarina/internal/schema"
1011
"github.com/golang-malawi/qatarina/internal/services"
@@ -93,6 +94,8 @@ func CreateProject(projectService services.ProjectService) fiber.Handler {
9394
return problemdetail.ValidationErrors(ctx, "invalid data in the request", err)
9495
}
9596

97+
request.ProjectOwnerID = authutil.GetAuthUserID(ctx)
98+
9699
project, err := projectService.Create(context.Background(), &request)
97100
if err != nil {
98101
return problemdetail.ServerErrorProblem(ctx, "failed to process request")

‎internal/schema/project.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type NewProjectRequest struct {
1111
WebsiteURL string `json:"website_url,omitempty" validate:"required"`
1212
Version string `json:"version" validate:"required"`
1313
GitHubURL string `json:"github_url,omitempty" validate:""`
14-
ProjectOwnerID int64 `json:"project_owner_id,omitempty" validate:"required"`
14+
ProjectOwnerID int64 `json:"project_owner_id,omitempty" validate:"-"`
1515
ParentProjectID int64 `json:"parent_project_id,omitempty"`
1616
}
1717

‎ui/index.html

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Vite + React + TS</title>
8-
</head>
9-
<body>
10-
<div id="root"></div>
11-
<script type="module" src="/src/main.tsx"></script>
12-
</body>
13-
</html>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>QATARINA</title>
9+
</head>
10+
11+
<body>
12+
<div id="root"></div>
13+
<script type="module" src="/src/main.tsx"></script>
14+
</body>
15+
16+
</html>

‎ui/package-lock.json

+2,660-161
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/package.json

+10-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"axios": "^1.7.2",
13+
"@chakra-ui/react": "^2.8.2",
14+
"@tabler/icons-react": "^3.14.0",
15+
"@tanstack/react-form": "^0.31.0",
16+
"@tanstack/react-query": "^5.53.3",
17+
"@tanstack/react-table": "^8.20.5",
18+
"@tanstack/zod-form-adapter": "^0.31.0",
19+
"axios": "^1.7.7",
1420
"fast-base64": "^0.1.8",
1521
"react": "^18.3.1",
1622
"react-dom": "^18.3.1",
1723
"react-helmet": "^6.1.0",
18-
"react-router": "^6.24.0"
24+
"react-router": "^6.24.0",
25+
"react-router-dom": "^6.26.1",
26+
"zod": "^3.23.8"
1927
},
2028
"devDependencies": {
2129
"@types/react": "^18.3.3",

‎ui/src/App.css

-42
Original file line numberDiff line numberDiff line change
@@ -1,42 +0,0 @@
1-
#root {
2-
max-width: 1280px;
3-
margin: 0 auto;
4-
padding: 2rem;
5-
text-align: center;
6-
}
7-
8-
.logo {
9-
height: 6em;
10-
padding: 1.5em;
11-
will-change: filter;
12-
transition: filter 300ms;
13-
}
14-
.logo:hover {
15-
filter: drop-shadow(0 0 2em #646cffaa);
16-
}
17-
.logo.react:hover {
18-
filter: drop-shadow(0 0 2em #61dafbaa);
19-
}
20-
21-
@keyframes logo-spin {
22-
from {
23-
transform: rotate(0deg);
24-
}
25-
to {
26-
transform: rotate(360deg);
27-
}
28-
}
29-
30-
@media (prefers-reduced-motion: no-preference) {
31-
a:nth-of-type(2) .logo {
32-
animation: logo-spin infinite 20s linear;
33-
}
34-
}
35-
36-
.card {
37-
padding: 2em;
38-
}
39-
40-
.read-the-docs {
41-
color: #888;
42-
}

‎ui/src/App.tsx

+37-28
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from '/vite.svg'
4-
import './App.css'
1+
import {
2+
createBrowserRouter,
3+
createRoutesFromElements,
4+
Route,
5+
RouterProvider
6+
} from "react-router-dom";
7+
import './App.css';
8+
import AuthLayout from "./app/AuthLayout";
9+
import DashboardPage from "./app/dashboard";
10+
import Home from "./app/Home";
11+
import Root from "./app/Layout";
12+
import LoginPage from "./app/login";
13+
import ProjectPage from "./app/projects";
14+
import CreateProject from "./app/projects/CreateProject";
515

6-
function App() {
7-
const [count, setCount] = useState(0)
816

17+
function App() {
918
return (
10-
<>
11-
<div>
12-
<a href="https://vitejs.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
18-
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.tsx</code> and save to test HMR
26-
</p>
27-
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
31-
</>
19+
<RouterProvider router={// Configure nested routes with JSX
20+
createBrowserRouter(
21+
createRoutesFromElements(
22+
<Route path="/" element={<Root />}>
23+
<Route path="" element={<Home />} />
24+
<Route path="projects" element={<ProjectPage />} />
25+
<Route path="/projects/new" element={<CreateProject />} />
26+
<Route
27+
path="dashboard"
28+
element={<DashboardPage />}
29+
/>
30+
<Route element={<AuthLayout />}>
31+
<Route
32+
path="login"
33+
element={<LoginPage />}
34+
// loader={redirectIfUser}
35+
/>
36+
{/* <Route path="logout" action={logoutUser} /> */}
37+
</Route>
38+
</Route>
39+
)
40+
)} />
3241
)
3342
}
3443

‎ui/src/app/AuthLayout.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Outlet } from "react-router";
2+
3+
export default function AuthLayout() {
4+
return (
5+
<Outlet />
6+
)
7+
}

‎ui/src/app/Home.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { IconDashboard, IconList, IconPlayerPlay, IconReport, IconSettings, IconTestPipe, IconUsersGroup } from "@tabler/icons-react";
2+
import { Link } from "react-router-dom";
3+
4+
export default function Home() {
5+
return (
6+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
7+
<h1>Home </h1>
8+
<ul>
9+
<li><Link to="/dashboard"><IconDashboard /> Dashboard</Link></li>
10+
<li><Link to="/projects"><IconList /> Projects</Link></li>
11+
<li><Link to="/testers"><IconUsersGroup /> Testers</Link></li>
12+
<li><Link to="/test-cases"><IconTestPipe /> Test Cases</Link></li>
13+
<li><Link to="/test-runs"><IconPlayerPlay /> Test Runs</Link></li>
14+
<li><Link to="/reports"><IconReport /> Reports</Link></li>
15+
<li><Link to="/insights"><IconSettings /> Settings</Link></li>
16+
</ul>
17+
</main>
18+
);
19+
}

‎ui/src/app/Layout.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Box, Flex } from "@chakra-ui/react";
2+
import { IconDashboard, IconList, IconPlayerPlay, IconReport, IconSettings, IconTestPipe, IconUsersGroup } from "@tabler/icons-react";
3+
import { Link, Outlet } from "react-router-dom";
4+
5+
export default function Home() {
6+
return (
7+
<main className="flex min-h-screen flex-col items-center justify-between p-24">
8+
<h1>QATARINA </h1>
9+
<Flex>
10+
<Box w="20%" borderRight={'1px solid rgba(200, 200, 200, 1)'} h="100vh">
11+
<ul>
12+
<li><Link to="/dashboard"><IconDashboard /> Dashboard</Link></li>
13+
<li><Link to="/projects"><IconList /> Projects</Link></li>
14+
<li><Link to="/testers"><IconUsersGroup /> Testers</Link></li>
15+
<li><Link to="/test-cases"><IconTestPipe /> Test Cases</Link></li>
16+
<li><Link to="/test-runs"><IconPlayerPlay /> Test Runs</Link></li>
17+
<li><Link to="/reports"><IconReport /> Reports</Link></li>
18+
<li><Link to="/insights"><IconSettings /> Settings</Link></li>
19+
</ul>
20+
</Box>
21+
<Box padding={4}>
22+
<Outlet />
23+
</Box>
24+
</Flex>
25+
</main>
26+
);
27+
}
File renamed without changes.

‎ui/src/app/layout.tsx

-56
This file was deleted.

‎ui/src/app/login/index.tsx

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
import { Box, Button, Input, InputGroup, Link } from '@chakra-ui/react';
3+
import axios from "axios";
4+
import { useState } from 'react';
5+
import { useNavigate } from "react-router";
6+
7+
interface LoginData {
8+
user_id: number
9+
token: string
10+
displayName: string
11+
}
12+
13+
export default function LoginPage() {
14+
const [email, setEmail] = useState('');
15+
const [password, setPassword] = useState('');
16+
const redirect = useNavigate();
17+
18+
async function handleSubmit(e: Event) {
19+
e.preventDefault()
20+
const res = await axios.post('http://localhost:4597/v1/auth/login', {
21+
email,
22+
password
23+
});
24+
25+
if (res.status == 200) {
26+
const loginData: LoginData = res.data;
27+
localStorage.setItem('auth.user_id', loginData.user_id);
28+
localStorage.setItem('auth.displayName', loginData.displayName);
29+
localStorage.setItem('auth.token', loginData.token);
30+
31+
axios.defaults.withCredentials = false;
32+
axios.defaults.headers.common["Authorization"] = `Bearer ${loginData.token}`;
33+
redirect('/dashboard');
34+
}
35+
return false;
36+
}
37+
return (
38+
<div>
39+
<p className="text-3xl">QATARINA</p>
40+
<h2>Login</h2>
41+
<form onSubmit={handleSubmit}>
42+
<Box>
43+
<InputGroup>
44+
<Input placeholder="E-mail" type="email" onChange={(e) => setEmail(e.target.value)} />
45+
<Input type="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
46+
</InputGroup>
47+
<Button type="submit">Login</Button>
48+
</Box>
49+
</form>
50+
<Link href="/forgot-password">Forgot Password?</Link>
51+
</div>
52+
)
53+
};

‎ui/src/app/login/page.tsx

-20
This file was deleted.

‎ui/src/app/page.tsx

-8
This file was deleted.

‎ui/src/app/projects/CreateProject.jsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
Button,
3+
FormControl,
4+
FormHelperText,
5+
FormLabel,
6+
Input,
7+
useToast
8+
} from "@chakra-ui/react";
9+
import axios from 'axios';
10+
import { useState } from "react";
11+
12+
13+
import { useNavigate } from "react-router";
14+
15+
export default function CreateProject() {
16+
const redirect = useNavigate();
17+
const toast = useToast();
18+
const [name, setName] = useState('');
19+
const [description, setDescription] = useState('');
20+
const [version, setVersion] = useState('');
21+
const [website_url, setWebsite_url] = useState('');
22+
23+
async function handleSubmit(e) {
24+
e.preventDefault();
25+
26+
const res = await axios.post('http://localhost:4597/v1/projects', {
27+
name,
28+
description,
29+
version,
30+
website_url
31+
})
32+
33+
if (res.status == 200) {
34+
toast({
35+
title: 'Project created.',
36+
description: "We've created your Project.",
37+
status: 'success',
38+
duration: 9000,
39+
isClosable: true,
40+
})
41+
redirect('/projects')
42+
}
43+
44+
return false;
45+
}
46+
47+
return (
48+
<form onSubmit={handleSubmit}>
49+
<FormControl>
50+
<FormLabel>Name</FormLabel>
51+
<Input type='text' name='name' onChange={(e) => setName(e.target.value)} />
52+
<FormHelperText>Project Title.</FormHelperText>
53+
</FormControl>
54+
55+
<FormControl>
56+
<FormLabel>Description</FormLabel>
57+
<Input type='text' name='description' onChange={(e) => setDescription(e.target.value)} />
58+
<FormHelperText>Project description.</FormHelperText>
59+
</FormControl>
60+
61+
<FormControl>
62+
<FormLabel>Version</FormLabel>
63+
<Input type='text' name='version' onChange={(e) => setVersion(e.target.value)} />
64+
<FormHelperText>Project version.</FormHelperText>
65+
</FormControl>
66+
67+
<FormControl>
68+
<FormLabel>Website URL</FormLabel>
69+
<Input type='text' name='website_url' onChange={(e) => setWebsite_url(e.target.value)} />
70+
<FormHelperText>Project version.</FormHelperText>
71+
</FormControl>
72+
73+
<Button type='submit'>Submit</Button>
74+
</form>
75+
)
76+
}

‎ui/src/app/projects/index.jsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Box, Button, Container, Flex } from "@chakra-ui/react";
2+
import { IconPlus, IconTrash } from "@tabler/icons-react";
3+
import axios from 'axios';
4+
import { useEffect, useState } from "react";
5+
import { Link } from 'react-router-dom';
6+
7+
export default function Projects() {
8+
const [records, setRecords] = useState([]);
9+
10+
useEffect(() => {
11+
12+
async function getProjects() {
13+
const res = await axios.get('http://localhost:4597/v1/projects')
14+
if (res.status == 200) {
15+
setRecords(res.data.projects)
16+
}
17+
}
18+
19+
getProjects()
20+
21+
return () => { }
22+
}, []);
23+
24+
const projectList = records.map(record => (
25+
<Container key={record.id} className="w-full">
26+
<h2>{record.title}</h2>
27+
<p>
28+
URL: <Link href={record.project_url}>{record.project_url}</Link>
29+
</p>
30+
<Flex>
31+
<Box>
32+
<Button bg="black" color="white">Manage</Button>
33+
</Box>
34+
<Box>
35+
<Button bg="blue.500" color="white">Add Testers</Button>
36+
</Box>
37+
<Box>
38+
<Button bg="blue" color="white">Start Test Session</Button>
39+
</Box>
40+
<Box>
41+
<Button bg="blue" color="white">Add Test Cases</Button>
42+
</Box>
43+
<Box>
44+
<Button bg="red" color="white"><IconTrash /></Button>
45+
</Box>
46+
</Flex>
47+
</Container>
48+
))
49+
50+
return (
51+
<div>
52+
Projects
53+
<Link to="/projects/new">
54+
<Button><IconPlus /> Create Project</Button>
55+
</Link>
56+
<hr />
57+
{projectList}
58+
</div>
59+
)
60+
}

‎ui/src/app/projects/page.tsx

-48
This file was deleted.

‎ui/src/index.css

-68
Original file line numberDiff line numberDiff line change
@@ -1,68 +0,0 @@
1-
:root {
2-
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3-
line-height: 1.5;
4-
font-weight: 400;
5-
6-
color-scheme: light dark;
7-
color: rgba(255, 255, 255, 0.87);
8-
background-color: #242424;
9-
10-
font-synthesis: none;
11-
text-rendering: optimizeLegibility;
12-
-webkit-font-smoothing: antialiased;
13-
-moz-osx-font-smoothing: grayscale;
14-
}
15-
16-
a {
17-
font-weight: 500;
18-
color: #646cff;
19-
text-decoration: inherit;
20-
}
21-
a:hover {
22-
color: #535bf2;
23-
}
24-
25-
body {
26-
margin: 0;
27-
display: flex;
28-
place-items: center;
29-
min-width: 320px;
30-
min-height: 100vh;
31-
}
32-
33-
h1 {
34-
font-size: 3.2em;
35-
line-height: 1.1;
36-
}
37-
38-
button {
39-
border-radius: 8px;
40-
border: 1px solid transparent;
41-
padding: 0.6em 1.2em;
42-
font-size: 1em;
43-
font-weight: 500;
44-
font-family: inherit;
45-
background-color: #1a1a1a;
46-
cursor: pointer;
47-
transition: border-color 0.25s;
48-
}
49-
button:hover {
50-
border-color: #646cff;
51-
}
52-
button:focus,
53-
button:focus-visible {
54-
outline: 4px auto -webkit-focus-ring-color;
55-
}
56-
57-
@media (prefers-color-scheme: light) {
58-
:root {
59-
color: #213547;
60-
background-color: #ffffff;
61-
}
62-
a:hover {
63-
color: #747bff;
64-
}
65-
button {
66-
background-color: #f9f9f9;
67-
}
68-
}

‎ui/src/main.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React from 'react'
22
import ReactDOM from 'react-dom/client'
33
import App from './App.tsx'
4+
import { Providers } from './app/providers.jsx'
45
import './index.css'
56

67
ReactDOM.createRoot(document.getElementById('root')!).render(
78
<React.StrictMode>
8-
<App />
9+
<Providers>
10+
<App />
11+
</Providers>
912
</React.StrictMode>,
1013
)

0 commit comments

Comments
 (0)
Please sign in to comment.