@getstrata/starter 0.1.4 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -20
- package/dist/cli.js +2400 -47
- package/dist/templates/.env.example +4 -1
- package/dist/templates/overlays/api/docs/API.md +9 -0
- package/dist/templates/overlays/server-htmx/public/assets/app.css +89 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/error.eta +14 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/forbidden.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/not-found.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/layouts/app.eta +34 -0
- package/dist/templates/overlays/server-htmx/resources/views/organizations/_table.eta +23 -0
- package/dist/templates/overlays/server-htmx/resources/views/organizations/index.eta +37 -0
- package/dist/templates/overlays/server-htmx/resources/views/pages/home.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/partials/_flash.eta +3 -0
- package/dist/templates/overlays/spa-react/frontend/build.ts +17 -0
- package/dist/templates/overlays/spa-react/frontend/bun-env.d.ts +4 -0
- package/dist/templates/overlays/spa-react/frontend/bun.lock +51 -0
- package/dist/templates/overlays/spa-react/frontend/dev-server.ts +66 -0
- package/dist/templates/overlays/spa-react/frontend/index.html +12 -0
- package/dist/templates/overlays/spa-react/frontend/package.json +21 -0
- package/dist/templates/overlays/spa-react/frontend/src/App.tsx +80 -0
- package/dist/templates/overlays/spa-react/frontend/src/api/client.ts +86 -0
- package/dist/templates/overlays/spa-react/frontend/src/app.css +98 -0
- package/dist/templates/overlays/spa-react/frontend/src/auth/AuthContext.tsx +103 -0
- package/dist/templates/overlays/spa-react/frontend/src/auth/tokenStorage.ts +15 -0
- package/dist/templates/overlays/spa-react/frontend/src/main.tsx +22 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/LoginPage.tsx +67 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/NotFoundPage.tsx +12 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/OrganizationsPage.tsx +69 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/ProjectsPage.tsx +65 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/TasksPage.tsx +65 -0
- package/dist/templates/{templates → overlays/spa-react/frontend}/tsconfig.json +9 -6
- package/dist/templates/package.json +2 -2
- package/package.json +3 -3
- package/dist/templates/templates/.env.example +0 -19
- package/dist/templates/templates/docker-compose.yml +0 -14
- package/dist/templates/templates/package.json +0 -22
- package/dist/templates/templates/public/assets/site.css +0 -29
- package/dist/templates/templates/src/bootstrap/config.ts +0 -18
- package/dist/templates/templates/src/bootstrap/createApp.ts +0 -93
- package/dist/templates/templates/src/bootstrap/database.ts +0 -30
- package/dist/templates/templates/src/bootstrap/preload.ts +0 -5
- package/dist/templates/templates/src/bootstrap/providers/auth.ts +0 -41
- package/dist/templates/templates/src/bootstrap/providers/cache.ts +0 -28
- package/dist/templates/templates/src/bootstrap/providers/config.ts +0 -27
- package/dist/templates/templates/src/bootstrap/providers/index.ts +0 -14
- package/dist/templates/templates/src/bootstrap/providers/storage.ts +0 -11
- package/dist/templates/templates/src/bootstrap/server.ts +0 -25
- package/dist/templates/templates/src/db/fresh.ts +0 -19
- package/dist/templates/templates/src/db/migrate.ts +0 -35
- package/dist/templates/templates/src/lib/view.ts +0 -21
- package/dist/templates/templates/src/modules/site/index.ts +0 -29
- package/dist/templates/templates/src/routes.ts +0 -6
- package/dist/templates/templates/strata.config.ts +0 -7
- package/dist/templates/templates/views/home.eta +0 -5
- package/dist/templates/templates/views/layouts/app.eta +0 -18
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #0f172a;
|
|
3
|
+
background: #f8fafc;
|
|
4
|
+
font-family: system-ui, sans-serif;
|
|
5
|
+
line-height: 1.5;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
a {
|
|
17
|
+
color: #2563eb;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.app-shell {
|
|
21
|
+
max-width: 960px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
padding: 1.5rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.site-header {
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
margin-bottom: 1.5rem;
|
|
32
|
+
padding-bottom: 1rem;
|
|
33
|
+
border-bottom: 1px solid #e2e8f0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.site-header nav {
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: 1rem;
|
|
39
|
+
flex-wrap: wrap;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.card {
|
|
43
|
+
background: #fff;
|
|
44
|
+
border: 1px solid #e2e8f0;
|
|
45
|
+
border-radius: 0.75rem;
|
|
46
|
+
padding: 1rem;
|
|
47
|
+
margin-bottom: 1rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.stack-form {
|
|
51
|
+
display: grid;
|
|
52
|
+
gap: 0.75rem;
|
|
53
|
+
max-width: 24rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.stack-form label {
|
|
57
|
+
display: grid;
|
|
58
|
+
gap: 0.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.stack-form input {
|
|
62
|
+
padding: 0.5rem 0.75rem;
|
|
63
|
+
border: 1px solid #cbd5e1;
|
|
64
|
+
border-radius: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
button {
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
border: none;
|
|
70
|
+
border-radius: 0.5rem;
|
|
71
|
+
padding: 0.5rem 0.875rem;
|
|
72
|
+
background: #2563eb;
|
|
73
|
+
color: #fff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button.secondary {
|
|
77
|
+
background: #64748b;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.error {
|
|
81
|
+
color: #b91c1c;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
table {
|
|
85
|
+
width: 100%;
|
|
86
|
+
border-collapse: collapse;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
th,
|
|
90
|
+
td {
|
|
91
|
+
text-align: left;
|
|
92
|
+
padding: 0.5rem 0.75rem;
|
|
93
|
+
border-bottom: 1px solid #e2e8f0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.hint {
|
|
97
|
+
color: #64748b;
|
|
98
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { apiFetch } from "../api/client";
|
|
11
|
+
import { clearToken, readToken, writeToken } from "./tokenStorage";
|
|
12
|
+
|
|
13
|
+
interface AuthUser {
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
role: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AuthContextValue {
|
|
21
|
+
token: string | null;
|
|
22
|
+
user: AuthUser | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
login: (email: string, password: string) => Promise<void>;
|
|
25
|
+
logout: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
function AuthProvider({ children }: { children: ReactNode }) {
|
|
31
|
+
const [token, setToken] = useState<string | null>(() => readToken());
|
|
32
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
33
|
+
const [loading, setLoading] = useState(Boolean(readToken()));
|
|
34
|
+
|
|
35
|
+
const loadProfile = useCallback(async (activeToken: string) => {
|
|
36
|
+
const profile = await apiFetch<AuthUser>("/auth/me", { token: activeToken });
|
|
37
|
+
setUser(profile);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!token) {
|
|
42
|
+
setUser(null);
|
|
43
|
+
setLoading(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadProfile(token)
|
|
48
|
+
.catch(() => {
|
|
49
|
+
clearToken();
|
|
50
|
+
setToken(null);
|
|
51
|
+
setUser(null);
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
});
|
|
56
|
+
}, [loadProfile, token]);
|
|
57
|
+
|
|
58
|
+
const login = useCallback(
|
|
59
|
+
async (email: string, password: string) => {
|
|
60
|
+
const body = await apiFetch<{ token: string }>("/auth/login", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({ email, password }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
writeToken(body.token);
|
|
66
|
+
setToken(body.token);
|
|
67
|
+
await loadProfile(body.token);
|
|
68
|
+
},
|
|
69
|
+
[loadProfile],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const logout = useCallback(() => {
|
|
73
|
+
clearToken();
|
|
74
|
+
setToken(null);
|
|
75
|
+
setUser(null);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const value = useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
token,
|
|
81
|
+
user,
|
|
82
|
+
loading,
|
|
83
|
+
login,
|
|
84
|
+
logout,
|
|
85
|
+
}),
|
|
86
|
+
[loading, login, logout, token, user],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useAuth(): AuthContextValue {
|
|
93
|
+
const context = useContext(AuthContext);
|
|
94
|
+
|
|
95
|
+
if (!context) {
|
|
96
|
+
throw new Error("useAuth must be used within AuthProvider");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return context;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type { AuthUser };
|
|
103
|
+
export { AuthProvider, useAuth };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const TOKEN_KEY = "strata_api_token";
|
|
2
|
+
|
|
3
|
+
function readToken(): string | null {
|
|
4
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function writeToken(token: string): void {
|
|
8
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clearToken(): void {
|
|
12
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { clearToken, readToken, TOKEN_KEY, writeToken };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
import App from "./App";
|
|
5
|
+
import { AuthProvider } from "./auth/AuthContext";
|
|
6
|
+
import "./app.css";
|
|
7
|
+
|
|
8
|
+
const rootElement = document.getElementById("root");
|
|
9
|
+
|
|
10
|
+
if (!rootElement) {
|
|
11
|
+
throw new Error("Root element #root not found");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createRoot(rootElement).render(
|
|
15
|
+
<StrictMode>
|
|
16
|
+
<BrowserRouter basename="/app">
|
|
17
|
+
<AuthProvider>
|
|
18
|
+
<App />
|
|
19
|
+
</AuthProvider>
|
|
20
|
+
</BrowserRouter>
|
|
21
|
+
</StrictMode>,
|
|
22
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type FormEvent, useState } from "react";
|
|
2
|
+
import { Navigate, useNavigate } from "react-router-dom";
|
|
3
|
+
import { ApiError } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
export default function LoginPage() {
|
|
7
|
+
const { login, token } = useAuth();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const [email, setEmail] = useState("admin@example.test");
|
|
10
|
+
const [password, setPassword] = useState("password");
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const [submitting, setSubmitting] = useState(false);
|
|
13
|
+
|
|
14
|
+
if (token) {
|
|
15
|
+
return <Navigate to="/organizations" replace />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
setSubmitting(true);
|
|
21
|
+
setError(null);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await login(email, password);
|
|
25
|
+
navigate("/organizations");
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
setError(cause instanceof ApiError ? cause.message : "Login failed");
|
|
28
|
+
} finally {
|
|
29
|
+
setSubmitting(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<section className="card">
|
|
35
|
+
<h1>Sign in</h1>
|
|
36
|
+
<p className="hint">Use seeded credentials or your own API user.</p>
|
|
37
|
+
|
|
38
|
+
{error ? <p className="error">{error}</p> : null}
|
|
39
|
+
|
|
40
|
+
<form className="stack-form" onSubmit={onSubmit}>
|
|
41
|
+
<label>
|
|
42
|
+
Email
|
|
43
|
+
<input
|
|
44
|
+
type="email"
|
|
45
|
+
value={email}
|
|
46
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</label>
|
|
50
|
+
|
|
51
|
+
<label>
|
|
52
|
+
Password
|
|
53
|
+
<input
|
|
54
|
+
type="password"
|
|
55
|
+
value={password}
|
|
56
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
57
|
+
required
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
|
|
61
|
+
<button type="submit" disabled={submitting}>
|
|
62
|
+
{submitting ? "Signing in…" : "Sign in"}
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Organization {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PaginatedOrganizations {
|
|
13
|
+
data: Organization[];
|
|
14
|
+
meta: {
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function OrganizationsPage() {
|
|
20
|
+
const { token } = useAuth();
|
|
21
|
+
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!token) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
apiFetch<PaginatedOrganizations>("/organizations", { token })
|
|
30
|
+
.then((body) => setOrganizations(body.data))
|
|
31
|
+
.catch((cause: unknown) => {
|
|
32
|
+
setError(cause instanceof Error ? cause.message : "Failed to load organizations");
|
|
33
|
+
});
|
|
34
|
+
}, [token]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section>
|
|
38
|
+
<h1>Organizations</h1>
|
|
39
|
+
<p className="hint">{organizations.length} loaded from the JSON API.</p>
|
|
40
|
+
|
|
41
|
+
{error ? <p className="error">{error}</p> : null}
|
|
42
|
+
|
|
43
|
+
<div className="card">
|
|
44
|
+
<table>
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th>Name</th>
|
|
48
|
+
<th>Slug</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{organizations.map((organization) => (
|
|
53
|
+
<tr key={organization.id}>
|
|
54
|
+
<td>{organization.name}</td>
|
|
55
|
+
<td>
|
|
56
|
+
<code>{organization.slug}</code>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
))}
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<p>
|
|
65
|
+
Continue to <Link to="/projects">projects</Link> or <Link to="/tasks">tasks</Link>.
|
|
66
|
+
</p>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Project {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
status: string;
|
|
10
|
+
organization_id: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PaginatedProjects {
|
|
14
|
+
data: Project[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ProjectsPage() {
|
|
18
|
+
const { token } = useAuth();
|
|
19
|
+
const [projects, setProjects] = useState<Project[]>([]);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!token) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
apiFetch<PaginatedProjects>("/projects?include=organization", { token })
|
|
28
|
+
.then((body) => setProjects(body.data))
|
|
29
|
+
.catch((cause: unknown) => {
|
|
30
|
+
setError(cause instanceof Error ? cause.message : "Failed to load projects");
|
|
31
|
+
});
|
|
32
|
+
}, [token]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section>
|
|
36
|
+
<h1>Projects</h1>
|
|
37
|
+
{error ? <p className="error">{error}</p> : null}
|
|
38
|
+
|
|
39
|
+
<div className="card">
|
|
40
|
+
<table>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th>Name</th>
|
|
44
|
+
<th>Status</th>
|
|
45
|
+
<th>Organization</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
{projects.map((project) => (
|
|
50
|
+
<tr key={project.id}>
|
|
51
|
+
<td>{project.name}</td>
|
|
52
|
+
<td>{project.status}</td>
|
|
53
|
+
<td>{project.organization_id}</td>
|
|
54
|
+
</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p>
|
|
61
|
+
Back to <Link to="/organizations">organizations</Link>.
|
|
62
|
+
</p>
|
|
63
|
+
</section>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Task {
|
|
7
|
+
id: number;
|
|
8
|
+
title: string;
|
|
9
|
+
status: string;
|
|
10
|
+
project_id: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PaginatedTasks {
|
|
14
|
+
data: Task[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function TasksPage() {
|
|
18
|
+
const { token } = useAuth();
|
|
19
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!token) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
apiFetch<PaginatedTasks>("/tasks", { token })
|
|
28
|
+
.then((body) => setTasks(body.data))
|
|
29
|
+
.catch((cause: unknown) => {
|
|
30
|
+
setError(cause instanceof Error ? cause.message : "Failed to load tasks");
|
|
31
|
+
});
|
|
32
|
+
}, [token]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section>
|
|
36
|
+
<h1>Tasks</h1>
|
|
37
|
+
{error ? <p className="error">{error}</p> : null}
|
|
38
|
+
|
|
39
|
+
<div className="card">
|
|
40
|
+
<table>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th>Title</th>
|
|
44
|
+
<th>Status</th>
|
|
45
|
+
<th>Project</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
{tasks.map((task) => (
|
|
50
|
+
<tr key={task.id}>
|
|
51
|
+
<td>{task.title}</td>
|
|
52
|
+
<td>{task.status}</td>
|
|
53
|
+
<td>{task.project_id}</td>
|
|
54
|
+
</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p>
|
|
61
|
+
Back to <Link to="/organizations">organizations</Link>.
|
|
62
|
+
</p>
|
|
63
|
+
</section>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
4
5
|
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
6
|
"skipLibCheck": true,
|
|
8
|
-
"
|
|
7
|
+
"moduleResolution": "bundler",
|
|
9
8
|
"allowImportingTsExtensions": true,
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"types": ["bun"]
|
|
12
15
|
},
|
|
13
|
-
"include": ["src
|
|
16
|
+
"include": ["src", "build.ts", "dev-server.ts", "bun-env.d.ts"]
|
|
14
17
|
}
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"check": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@getstrata/bootstrap": "^0.2
|
|
14
|
+
"@getstrata/bootstrap": "^0.4.2",
|
|
15
15
|
"@getstrata/cli": "^0.2.0",
|
|
16
|
-
"@getstrata/core": "^0.
|
|
16
|
+
"@getstrata/core": "^0.7.4"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/bun": "^1.4.0",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/starter",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Interactive create-strata wizard. Choose each layer; one database engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "bun build cli.ts --outdir dist --target bun && cp -r templates dist/templates",
|
|
20
|
+
"build": "bun build cli.ts --outdir dist --target bun && rm -rf dist/templates && mkdir -p dist/templates/overlays && cp -r templates/. dist/templates && cp -r ../../templates/scaffold/. dist/templates/overlays",
|
|
21
21
|
"prepublishOnly": "bun run build"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
|
|
2
|
-
PORT=3000
|
|
3
|
-
APP_URL=http://localhost:3000
|
|
4
|
-
APP_ENV=local
|
|
5
|
-
APP_NAME={{PROJECT_NAME}}
|
|
6
|
-
FRONTEND_MODE=api
|
|
7
|
-
TENANCY_DRIVER=none
|
|
8
|
-
|
|
9
|
-
# Required in production when FRONTEND_MODE=server-htmx
|
|
10
|
-
# SESSION_SECRET=
|
|
11
|
-
|
|
12
|
-
# Set only behind a trusted reverse proxy
|
|
13
|
-
# TRUST_FORWARDED_FOR=true
|
|
14
|
-
|
|
15
|
-
# Required in production to expose GET /metrics
|
|
16
|
-
# METRICS_TOKEN=
|
|
17
|
-
|
|
18
|
-
# Production should set FEATURE_PUBLIC_READS=false
|
|
19
|
-
# FEATURE_PUBLIC_READS=false
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{PROJECT_NAME}}",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "strata dev",
|
|
8
|
-
"start": "strata start",
|
|
9
|
-
"db:migrate": "strata migrate",
|
|
10
|
-
"db:fresh": "strata migrate:fresh",
|
|
11
|
-
"check": "tsc --noEmit"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@getstrata/bootstrap": "^0.2.53",
|
|
15
|
-
"@getstrata/cli": "^0.2.0",
|
|
16
|
-
"@getstrata/core": "^0.5.62"
|
|
17
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@types/bun": "^1.4.0",
|
|
20
|
-
"typescript": "^5.9.2"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color-scheme: light dark;
|
|
3
|
-
font-family: system-ui, sans-serif;
|
|
4
|
-
line-height: 1.5;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
margin: 0;
|
|
9
|
-
padding: 0 1.5rem 2rem;
|
|
10
|
-
max-width: 48rem;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.site-header {
|
|
14
|
-
padding: 1rem 0;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.brand {
|
|
18
|
-
font-weight: 700;
|
|
19
|
-
text-decoration: none;
|
|
20
|
-
color: inherit;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.section h1 {
|
|
24
|
-
margin-top: 0;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
code {
|
|
28
|
-
font-size: 0.9em;
|
|
29
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface AppConfig {
|
|
2
|
-
port: number;
|
|
3
|
-
appUrl: string;
|
|
4
|
-
databaseUrl: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function loadConfig(): AppConfig {
|
|
8
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
9
|
-
if (!databaseUrl) {
|
|
10
|
-
throw new Error("DATABASE_URL is required");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
port: Number(process.env.PORT ?? 3000),
|
|
15
|
-
appUrl: process.env.APP_URL ?? "http://localhost:3000",
|
|
16
|
-
databaseUrl,
|
|
17
|
-
};
|
|
18
|
-
}
|