Frontend form handling done
This commit is contained in:
@@ -7,6 +7,9 @@ WORKDIR /app
|
|||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
|
# ---- Production mode ----
|
||||||
|
FROM base AS prod
|
||||||
|
|
||||||
# Copy code, and compile
|
# Copy code, and compile
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build -o server main.go
|
RUN go build -o server main.go
|
||||||
@@ -17,3 +20,10 @@ EXPOSE 8080
|
|||||||
# Run server binary on start
|
# Run server binary on start
|
||||||
CMD ["./server"]
|
CMD ["./server"]
|
||||||
|
|
||||||
|
# ---- Development mode ----
|
||||||
|
FROM base AS dev
|
||||||
|
|
||||||
|
# Install air for hot reloading
|
||||||
|
RUN go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
52
backend/.air.toml
Normal file
52
backend/.air.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
34
backend/controllers/users.go
Normal file
34
backend/controllers/users.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct{}
|
||||||
|
|
||||||
|
type RegisterForm struct {
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
RepeatPassword string `json:"repeatPassword" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Register(c *gin.Context) {
|
||||||
|
// Receive data from frontend, check if data is okay, hash password, call model
|
||||||
|
var data RegisterForm
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
// TODO: Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
// Handle error
|
||||||
|
log.Fatalf("Error: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Println(data)
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
-- Create users table if it doesn't exist
|
-- Create users table if it doesn't exist
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
username TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ func main() {
|
|||||||
routes := routes.SetupRoutes()
|
routes := routes.SetupRoutes()
|
||||||
|
|
||||||
// Listen on port smth
|
// Listen on port smth
|
||||||
log.Printf("Starting server on %s PORT\n", env["port"])
|
log.Printf("Starting server ...")
|
||||||
log.Fatal(routes.Run(":8080"))
|
log.Fatal(routes.Run(":8080"))
|
||||||
}
|
}
|
||||||
|
|||||||
13
backend/models/users.go
Normal file
13
backend/models/users.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int
|
||||||
|
email string
|
||||||
|
name string
|
||||||
|
password string
|
||||||
|
createdAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(email string, name string, hash string) {
|
||||||
|
// TODO: Insert user into database
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"backend/controllers"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
func SetupRoutes() *gin.Engine {
|
func SetupRoutes() *gin.Engine {
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/ping", func(c *gin.Context) {
|
// Controllers
|
||||||
c.JSON(http.StatusOK, gin.H{
|
users := controllers.User{}
|
||||||
"message": "pong",
|
|
||||||
})
|
// Guest routes (Register, Login, check auth)
|
||||||
})
|
r.POST("/register", users.Register)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
1
backend/utils/responses.go
Normal file
1
backend/utils/responses.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package utils
|
||||||
@@ -61,8 +61,8 @@ services:
|
|||||||
- ./data/db:/var/lib/postgresql/data
|
- ./data/db:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
interval: 5s
|
interval: 2s
|
||||||
timeout: 5s
|
timeout: 2s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const config = {
|
const config = {
|
||||||
trailingComma: "es5",
|
trailingComma: "es5",
|
||||||
printWidth: 120,
|
printWidth: 110,
|
||||||
tabWidth: 4,
|
tabWidth: 4,
|
||||||
semi: true,
|
semi: true,
|
||||||
singleQuote: false,
|
singleQuote: false,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
@@ -536,6 +537,8 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||||
|
|
||||||
|
"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
|
||||||
|
|
||||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hot-toast": "^2.5.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
|
|||||||
48
frontend/src/components/forms/TextField.tsx
Normal file
48
frontend/src/components/forms/TextField.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useFieldContext } from "@/hooks/formHook";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
|
||||||
|
// --- field components ---
|
||||||
|
interface TextFieldProps {
|
||||||
|
placeholder?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: React.ComponentProps<"input">["type"];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TextField({
|
||||||
|
placeholder,
|
||||||
|
type = "text",
|
||||||
|
className = "",
|
||||||
|
label = "",
|
||||||
|
}: TextFieldProps) {
|
||||||
|
// Get field with predefined text type
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
|
||||||
|
// Render custom field
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col", className)}>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={field.name} className="ml-1 mb-2 text-secondary-foreground text-sm">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder || `${type} input`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!field.state.meta.isValid && (
|
||||||
|
<span className="text-xs text-danger mt-1">
|
||||||
|
{field.state.meta.errors.map((e) => e.message).join(", ")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
frontend/src/consts.ts
Normal file
2
frontend/src/consts.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const BASE = "/";
|
||||||
|
export const API_BASE = `${BASE}/api/`;
|
||||||
14
frontend/src/hooks/formHook.tsx
Normal file
14
frontend/src/hooks/formHook.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import TextField from "@/components/forms/TextField";
|
||||||
|
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
|
||||||
|
|
||||||
|
// export useFieldContext for use in your custom components
|
||||||
|
export const { fieldContext, formContext, useFieldContext } = createFormHookContexts();
|
||||||
|
|
||||||
|
export const { useAppForm } = createFormHook({
|
||||||
|
fieldComponents: {
|
||||||
|
TextField,
|
||||||
|
},
|
||||||
|
formComponents: {},
|
||||||
|
fieldContext,
|
||||||
|
formContext,
|
||||||
|
});
|
||||||
63
frontend/src/lib/requests.ts
Normal file
63
frontend/src/lib/requests.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { API_BASE } from "@/consts";
|
||||||
|
import { normalizeLink } from "./utils";
|
||||||
|
import type { ApiResponse } from "@/types/api";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface RequestProps<T> {
|
||||||
|
error?: (err: Error) => void;
|
||||||
|
success?: (data: T) => void;
|
||||||
|
before?: () => void;
|
||||||
|
finally?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostProps<T> extends RequestProps<T> {
|
||||||
|
data: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Requests {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async post<T>(url: string, props: PostProps<T>): Promise<T | void> {
|
||||||
|
props.before?.();
|
||||||
|
|
||||||
|
// Normalize url
|
||||||
|
const finalUrl = normalizeLink(`${API_BASE}/${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Do request
|
||||||
|
const res = await fetch(finalUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(props.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get response data
|
||||||
|
const data = (await res.json()) as ApiResponse<T>;
|
||||||
|
|
||||||
|
// Check if data is ok
|
||||||
|
if ("success" in data && !data.success) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another check for unexpected error
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Unexpected API ERROR with code: " + res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return response data
|
||||||
|
props.success?.(data.data);
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
// Show notification, and call error callback
|
||||||
|
toast.error(err.message);
|
||||||
|
props.error?.(err);
|
||||||
|
} finally {
|
||||||
|
props.finally?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Requests();
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeLink(link: string) {
|
||||||
|
let tmp = link;
|
||||||
|
|
||||||
|
// Remove double slashes
|
||||||
|
while (tmp.includes("//")) {
|
||||||
|
tmp = tmp.replaceAll("//", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { routeTree } from "./routeTree.gen";
|
|||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
import reportWebVitals from "./reportWebVitals.ts";
|
import reportWebVitals from "./reportWebVitals.ts";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -34,11 +35,11 @@ const rootElement = document.getElementById("app");
|
|||||||
if (rootElement && !rootElement.innerHTML) {
|
if (rootElement && !rootElement.innerHTML) {
|
||||||
const root = ReactDOM.createRoot(rootElement);
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<TanStackQueryProvider.Provider>
|
||||||
<TanStackQueryProvider.Provider>
|
{/* Toaster for notifications */}
|
||||||
<RouterProvider router={router} />
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
</TanStackQueryProvider.Provider>
|
<RouterProvider router={router} />
|
||||||
</StrictMode>
|
</TanStackQueryProvider.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { useAppForm } from "@/hooks/formHook";
|
||||||
import Guest from "@/layouts/Guest";
|
import Guest from "@/layouts/Guest";
|
||||||
import { useForm } from "@tanstack/react-form";
|
|
||||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
@@ -13,8 +12,9 @@ export const Route = createFileRoute("/register")({
|
|||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(8),
|
name: z.string().min(2, "Name is too short"),
|
||||||
repeatPassword: z.string().min(8),
|
password: z.string().min(8, "Password is too short"),
|
||||||
|
repeatPassword: z.string().min(8, "Password is too short"),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.repeatPassword, {
|
.refine((data) => data.password === data.repeatPassword, {
|
||||||
message: "Passwords don't match",
|
message: "Passwords don't match",
|
||||||
@@ -22,10 +22,11 @@ const registerSchema = z
|
|||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const form = useForm({
|
const form = useAppForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
name: "",
|
||||||
repeatPassword: "",
|
repeatPassword: "",
|
||||||
},
|
},
|
||||||
validators: {
|
validators: {
|
||||||
@@ -36,6 +37,10 @@ function RouteComponent() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// requests.post<{ message: string }>("/ping", { data: { hello: "world" } });
|
||||||
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Guest className="h-screen w-screen grid place-items-center">
|
<Guest className="h-screen w-screen grid place-items-center">
|
||||||
<Card className="w-full max-w-[400px]">
|
<Card className="w-full max-w-[400px]">
|
||||||
@@ -48,23 +53,47 @@ function RouteComponent() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-3">
|
||||||
<form.Field
|
<form.AppField
|
||||||
name="email"
|
name="email"
|
||||||
children={(field) => (
|
children={(field) => (
|
||||||
<Input
|
<field.TextField
|
||||||
id={field.name}
|
label="Email address"
|
||||||
name={field.name}
|
placeholder="Your email address"
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email address"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Input type="password" placeholder="Password" />
|
|
||||||
<Input type="password" placeholder="Repeat password" />
|
<form.AppField
|
||||||
|
name="name"
|
||||||
|
children={(field) => (
|
||||||
|
<field.TextField label="Name" placeholder="Full name or nickname" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.AppField
|
||||||
|
name="password"
|
||||||
|
children={(field) => (
|
||||||
|
<field.TextField
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your accounts password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<form.AppField
|
||||||
|
name="repeatPassword"
|
||||||
|
children={(field) => (
|
||||||
|
<field.TextField
|
||||||
|
label="Repeat password"
|
||||||
|
placeholder="Repeat your password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button onClick={form.handleSubmit} className="w-full">
|
<Button onClick={form.handleSubmit} className="w-full">
|
||||||
Register
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
/* My own variables */
|
/* My own variables */
|
||||||
--color-body: var(--background);
|
--color-body: var(--background);
|
||||||
--color-panel: var(--card);
|
--color-panel: var(--card);
|
||||||
|
--color-danger: var(--destructive);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
13
frontend/src/types/api.ts
Normal file
13
frontend/src/types/api.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Data structure for successful responses
|
||||||
|
export interface SuccessResponse<T> {
|
||||||
|
success: true;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data structure for error responses
|
||||||
|
export interface ErrorResponse {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
|
||||||
6
scripts/start.sh
Executable file
6
scripts/start.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Load variables
|
||||||
|
source ./var.sh
|
||||||
|
|
||||||
|
cd .. && docker compose -f $file up -d --build
|
||||||
Reference in New Issue
Block a user