base ui
This commit is contained in:
parent
670fcc3303
commit
c79562ecf2
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/cmd/shared"
|
||||
|
@ -92,7 +93,10 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
|
|||
// Configure app routes
|
||||
server.Mux.Route("/api", func(r chi.Router) {
|
||||
// Set CORS headers for API routes in development mode
|
||||
r.Use(middleware.SetHeader("Access-Control-Allow-Origin", "*"))
|
||||
r.Use(middleware.SetHeader("Access-Control-Allow-Origin", "http://"+net.JoinHostPort(cfg.Host, cfg.UIPort)))
|
||||
r.Use(middleware.SetHeader("Access-Control-Allow-Methods", "*"))
|
||||
r.Use(middleware.SetHeader("Access-Control-Allow-Headers", "*"))
|
||||
r.Use(middleware.SetHeader("Access-Control-Allow-Credentials", "true"))
|
||||
r.Use(servermiddleware.Auth(userService))
|
||||
r.Mount("/", apiRouter)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"ignorePatterns": [
|
||||
"**/*.js"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"extends": [
|
||||
"@marolint/eslint-config-react"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
"@marolint/eslint-config-react/prettier.config"
|
File diff suppressed because it is too large
Load Diff
|
@ -10,19 +10,22 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@marolint/eslint-config-react": "^1.0.2",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"typescript": "^5.0.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.47.0",
|
||||
"postcss": "^8.4.28",
|
||||
"prettier": "^2.8.8",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -2,41 +2,4 @@
|
|||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
|
|
@ -1,41 +1,38 @@
|
|||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
import { FunctionComponent } from "react"
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
const incrCount = () => {
|
||||
setCount(count + 1)
|
||||
fetch(import.meta.env.VITE_API_URL).then((res) => {
|
||||
console.log(res)
|
||||
})
|
||||
}
|
||||
import "./App.css"
|
||||
import Button from "./components/Button"
|
||||
import { useIsAuthenticated } from "./hooks/useAuth"
|
||||
|
||||
const App: FunctionComponent = () => {
|
||||
const isAuthenticated = useIsAuthenticated()
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
<div className="flex flex-col">
|
||||
<div className="grid grid-cols-3 justify-between text-slate-500 font-medium">
|
||||
<div className="flex-flex-row mr-auto text-lg">
|
||||
<Button text="GoShort" link="/" />
|
||||
</div>
|
||||
<div className="flex flex-row mx-auto">
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<Button text="Shorts" link="shorts" />
|
||||
<Button text="Tokens" link="tokens" />
|
||||
<Button text="Sesions" link="sessions" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row ml-auto">
|
||||
{isAuthenticated || <Button text="Signup" link="signup" />}
|
||||
{isAuthenticated || <Button text="Login" link="login" />}
|
||||
{isAuthenticated && <Button text="Logout" link="logout" />}
|
||||
</div>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={incrCount}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
<div className="flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { FunctionComponent } from "react"
|
||||
|
||||
import classNames from "classnames"
|
||||
import { NavLink, useSearchParams } from "react-router-dom"
|
||||
|
||||
const Button: FunctionComponent<{
|
||||
text: string
|
||||
onClick?: () => void
|
||||
link?: string
|
||||
}> = ({ text, onClick, link }) => {
|
||||
const Content: FunctionComponent<{ active?: boolean }> = ({ active }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"px-0 mx-3 py-2 group border-b hover:border-blue-600 transition-colors duration-200",
|
||||
{ "border-b-2 border-blue-600": active, "border-transparent": !active }
|
||||
)}>
|
||||
<button
|
||||
className={classNames("group-hover:text-blue-600", {
|
||||
"text-blue-600": active,
|
||||
})}
|
||||
onClick={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Since a user may go from the login page to the signup page, we want to
|
||||
// preserve the `from` query parameter so that we can redirect the user back
|
||||
// to the page they were on before they logged in.
|
||||
const [searchParams] = useSearchParams()
|
||||
const from = searchParams.get("from")
|
||||
|
||||
if (link) {
|
||||
const linkWithFrom = from ? `${link}?from=${from}` : link
|
||||
return (
|
||||
<NavLink to={linkWithFrom}>
|
||||
{({ isActive }) => <Content active={isActive} />}
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
return <Content />
|
||||
}
|
||||
|
||||
export default Button
|
|
@ -0,0 +1,148 @@
|
|||
import { LoaderFunction, redirect, useRouteLoaderData } from "react-router-dom"
|
||||
|
||||
import { User } from "../types"
|
||||
import fetchAPI from "../util/fetchAPI"
|
||||
|
||||
export type AuthProviderType = {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
signup(username: string, password: string): Promise<boolean>
|
||||
login(username: string, password: string): Promise<boolean>
|
||||
logout(): Promise<boolean>
|
||||
}
|
||||
|
||||
export const AuthProvider: AuthProviderType = {
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
async signup(username, password) {
|
||||
const response = await fetchAPI("/signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
return this.login(username, password)
|
||||
}
|
||||
return false
|
||||
},
|
||||
async login(username, password) {
|
||||
const response = await fetchAPI<User>("/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
this.user = response.data
|
||||
this.isAuthenticated = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
async logout() {
|
||||
const response = await fetchAPI("/logout", {
|
||||
method: "POST",
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
this.user = null
|
||||
this.isAuthenticated = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
type IndexLoaderData = Pick<AuthProviderType, "user" | "isAuthenticated">
|
||||
|
||||
export const useUser = () =>
|
||||
(useRouteLoaderData("root") as IndexLoaderData | null)?.user ?? null
|
||||
|
||||
export const useIsAuthenticated = () =>
|
||||
(useRouteLoaderData("root") as IndexLoaderData | null)?.isAuthenticated ??
|
||||
false
|
||||
|
||||
export const indexLoader: LoaderFunction =
|
||||
async (): Promise<IndexLoaderData> => {
|
||||
const response = await fetchAPI<User>("/me")
|
||||
if (response.ok) {
|
||||
AuthProvider.user = response.data
|
||||
AuthProvider.isAuthenticated = true
|
||||
}
|
||||
return {
|
||||
user: AuthProvider.user,
|
||||
isAuthenticated: AuthProvider.isAuthenticated,
|
||||
}
|
||||
}
|
||||
|
||||
export const loginAction: LoaderFunction = async ({ request }) => {
|
||||
const formData = await request.formData()
|
||||
const username = formData.get("username") as string | null
|
||||
const password = formData.get("password") as string | null
|
||||
|
||||
// Validate our form inputs and return validation errors via useActionData()
|
||||
if (!username || !password) {
|
||||
return {
|
||||
error: "You must provide a username and password to log in",
|
||||
}
|
||||
}
|
||||
|
||||
const ok = await AuthProvider.login(username, password)
|
||||
// Sign in and redirect to the proper destination if successful.
|
||||
if (!ok) {
|
||||
return {
|
||||
error: "Invalid login attempt",
|
||||
}
|
||||
}
|
||||
|
||||
const redirectTo = formData.get("redirectTo") as string | null
|
||||
return redirect(redirectTo || "/")
|
||||
}
|
||||
export const loginLoader: LoaderFunction = async () => {
|
||||
if (AuthProvider.isAuthenticated) {
|
||||
return redirect("/")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const singupAction: LoaderFunction = async ({ request }) => {
|
||||
const formData = await request.formData()
|
||||
const username = formData.get("username") as string | null
|
||||
const password = formData.get("password") as string | null
|
||||
|
||||
// Validate our form inputs and return validation errors via useActionData()
|
||||
if (!username || !password) {
|
||||
return {
|
||||
error: "You must provide a username and password to sign up",
|
||||
}
|
||||
}
|
||||
|
||||
const ok = await AuthProvider.signup(username, password)
|
||||
// Sign in and redirect to the proper destination if successful.
|
||||
if (!ok) {
|
||||
return {
|
||||
error: "Invalid signup attempt",
|
||||
}
|
||||
}
|
||||
|
||||
const redirectTo = formData.get("redirectTo") as string | null
|
||||
return redirect(redirectTo || "/")
|
||||
}
|
||||
export const signupLoader = loginLoader
|
||||
|
||||
export const logoutLoader: LoaderFunction = async () => {
|
||||
await AuthProvider.logout()
|
||||
|
||||
return redirect("/")
|
||||
}
|
||||
|
||||
export const protectedLoader: LoaderFunction = async ({ request }) => {
|
||||
// If the user is not logged in and tries to access `/protected`, we redirect
|
||||
// them to `/login` with a `from` parameter that allows login to redirect back
|
||||
// to this page upon successful authentication
|
||||
if (!AuthProvider.isAuthenticated) {
|
||||
const params = new URLSearchParams()
|
||||
params.set("from", new URL(request.url).pathname)
|
||||
return redirect("/login?" + params.toString())
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -1,69 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
font-family: -apple-system, SF Pro Text, SF UI Text, system-ui, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { StrictMode } from "react"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { RouterProvider } from "react-router-dom"
|
||||
|
||||
import "./index.css"
|
||||
import router from "./router.tsx"
|
||||
|
||||
const rootEl = document.getElementById("root")
|
||||
|
||||
if (!rootEl) throw new Error("Root element not found")
|
||||
|
||||
createRoot(rootEl).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} fallbackElement={<p>Loading...</p>} />
|
||||
</StrictMode>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { FunctionComponent } from "react"
|
||||
|
||||
const IndexPage: FunctionComponent = () => {
|
||||
return <div>Index</div>
|
||||
}
|
||||
|
||||
export default IndexPage
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLocation,
|
||||
useNavigation,
|
||||
} from "react-router-dom"
|
||||
|
||||
export function Component() {
|
||||
const location = useLocation()
|
||||
const params = new URLSearchParams(location.search)
|
||||
const from = params.get("from") || "/"
|
||||
|
||||
const navigation = useNavigation()
|
||||
const isLoggingIn = navigation.formData?.get("username") != null
|
||||
|
||||
const actionData = useActionData() as { error: string } | undefined
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You must log in to view the page at {from}</p>
|
||||
|
||||
<Form method="post" replace>
|
||||
<input type="hidden" name="redirectTo" value={from} />
|
||||
<label>
|
||||
Username: <input name="username" />
|
||||
</label>{" "}
|
||||
<label>
|
||||
Password: <input type="password" name="password" />
|
||||
</label>{" "}
|
||||
<button type="submit" disabled={isLoggingIn}>
|
||||
{isLoggingIn ? "Logging in..." : "Login"}
|
||||
</button>
|
||||
{actionData && actionData.error ? (
|
||||
<p style={{ color: "red" }}>{actionData.error}</p>
|
||||
) : null}
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = "LoginPage"
|
|
@ -0,0 +1,7 @@
|
|||
import { FunctionComponent } from "react"
|
||||
|
||||
const NotFoundPage: FunctionComponent = () => {
|
||||
return <div>Not Found</div>
|
||||
}
|
||||
|
||||
export default NotFoundPage
|
|
@ -0,0 +1,49 @@
|
|||
import { LoaderFunction, json, useLoaderData } from "react-router-dom"
|
||||
|
||||
import { protectedLoader } from "../hooks/useAuth"
|
||||
|
||||
type Session = {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async (args) => {
|
||||
const redirect = await protectedLoader(args)
|
||||
if (redirect) return redirect
|
||||
|
||||
const data: Session[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Session 1",
|
||||
description: "Session 1 description",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Session 2",
|
||||
description: "Session 2 description",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Session 3",
|
||||
description: "Session 3 description",
|
||||
},
|
||||
]
|
||||
return json(data)
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const data = useLoaderData() as Session[]
|
||||
return (
|
||||
<div>
|
||||
<div>Sessions</div>
|
||||
<ul>
|
||||
{data.map((s) => (
|
||||
<li key={s.id}>{s.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = "SessionsPage"
|
|
@ -0,0 +1,37 @@
|
|||
import { LoaderFunction, redirect, useLoaderData } from "react-router-dom"
|
||||
|
||||
import { protectedLoader } from "../hooks/useAuth"
|
||||
import fetchAPI from "../util/fetchAPI"
|
||||
|
||||
type Short = {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async (args) => {
|
||||
const resp = await protectedLoader(args)
|
||||
if (resp) return resp
|
||||
|
||||
const data = await fetchAPI<Short[]>("/short")
|
||||
if (data.ok) {
|
||||
return data.data
|
||||
}
|
||||
return redirect("/logout")
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const data = (useLoaderData() ?? []) as Short[]
|
||||
console.log(data)
|
||||
return (
|
||||
<div>
|
||||
<div>Shorts</div>
|
||||
<ul>
|
||||
{data.map((s) => (
|
||||
<li key={s.name}>{s.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = "ShortsPage"
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLocation,
|
||||
useNavigation,
|
||||
} from "react-router-dom"
|
||||
|
||||
export function Component() {
|
||||
const location = useLocation()
|
||||
const params = new URLSearchParams(location.search)
|
||||
const from = params.get("from") || "/"
|
||||
|
||||
const navigation = useNavigation()
|
||||
const isLoggingIn = navigation.formData?.get("username") != null
|
||||
|
||||
const actionData = useActionData() as { error: string } | undefined
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>You must log in to view the page at {from}</p>
|
||||
|
||||
<Form method="post" replace>
|
||||
<input type="hidden" name="redirectTo" value={from} />
|
||||
<label>
|
||||
Username: <input name="username" />
|
||||
</label>{" "}
|
||||
<label>
|
||||
Password: <input type="password" name="password" />
|
||||
</label>{" "}
|
||||
<button type="submit" disabled={isLoggingIn}>
|
||||
{isLoggingIn ? "Logging in..." : "Login"}
|
||||
</button>
|
||||
{actionData && actionData.error ? (
|
||||
<p style={{ color: "red" }}>{actionData.error}</p>
|
||||
) : null}
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = "LoginPage"
|
|
@ -0,0 +1,36 @@
|
|||
import { LoaderFunction, json, useLoaderData } from "react-router-dom"
|
||||
|
||||
import { protectedLoader } from "../hooks/useAuth"
|
||||
|
||||
type Token = {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async (args) => {
|
||||
const redirect = await protectedLoader(args)
|
||||
if (redirect) return redirect
|
||||
|
||||
const data: Token[] = [
|
||||
{ id: 1, name: "Token 1" },
|
||||
{ id: 2, name: "Token 2" },
|
||||
{ id: 3, name: "Token 3" },
|
||||
]
|
||||
return json(data)
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const data = useLoaderData() as Token[]
|
||||
return (
|
||||
<div>
|
||||
<div>Tokens</div>
|
||||
<ul>
|
||||
{data.map((s) => (
|
||||
<li key={s.id}>{s.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Component.displayName = "TokensPage"
|
|
@ -0,0 +1,59 @@
|
|||
import { createBrowserRouter } from "react-router-dom"
|
||||
|
||||
import App from "./App"
|
||||
import {
|
||||
indexLoader,
|
||||
loginAction,
|
||||
loginLoader,
|
||||
logoutLoader,
|
||||
signupLoader,
|
||||
singupAction,
|
||||
} from "./hooks/useAuth"
|
||||
import IndexPage from "./pages/Index"
|
||||
import NotFound from "./pages/NotFound"
|
||||
|
||||
export default createBrowserRouter([
|
||||
{
|
||||
id: "root",
|
||||
path: "/",
|
||||
element: <App />,
|
||||
// Set the current user as the data for the root route, so we can access it
|
||||
// via our `useUser` hook.
|
||||
loader: indexLoader,
|
||||
children: [
|
||||
{ index: true, element: <IndexPage /> },
|
||||
{
|
||||
path: "login",
|
||||
lazy: () => import("./pages/Login"),
|
||||
loader: loginLoader,
|
||||
action: loginAction,
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
lazy: () => import("./pages/Signup"),
|
||||
loader: signupLoader,
|
||||
action: singupAction,
|
||||
},
|
||||
{
|
||||
path: "logout",
|
||||
loader: logoutLoader,
|
||||
},
|
||||
{
|
||||
path: "shorts",
|
||||
lazy: () => import("./pages/Shorts"),
|
||||
},
|
||||
{
|
||||
path: "tokens",
|
||||
lazy: () => import("./pages/Tokens"),
|
||||
},
|
||||
{
|
||||
path: "sessions",
|
||||
lazy: () => import("./pages/Sessions"),
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: <NotFound />,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
|
@ -0,0 +1,8 @@
|
|||
export type User = {
|
||||
username: string
|
||||
}
|
||||
|
||||
export type Short = {
|
||||
name: string
|
||||
url: string
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// Fetch function that automatically points to the API URL
|
||||
export default async function <T>(
|
||||
path: string,
|
||||
args: Parameters<typeof fetch>[1] = {}
|
||||
): Promise<{ data: T; ok: true } | { data: null; ok: false }> {
|
||||
args.credentials = "include"
|
||||
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_URL || ""}${path}`,
|
||||
args
|
||||
)
|
||||
|
||||
// if the response was not ok
|
||||
if (!response.ok) {
|
||||
console.error(response.statusText)
|
||||
|
||||
return { data: null, ok: false }
|
||||
}
|
||||
|
||||
// on a successfull response, return the response
|
||||
const data: T = await response.json()
|
||||
return { data, ok: true }
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
|
@ -30,4 +30,4 @@
|
|||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,5 +6,7 @@
|
|||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
Loading…
Reference in New Issue
Block a user