@getstrata/starter 1.0.0 → 1.0.2
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 +27 -9
- package/dist/cli.js +427 -167
- package/dist/templates/overlays/api/docs/API.md +45 -5
- package/dist/templates/overlays/spa-react/frontend/src/App.tsx +4 -25
- package/dist/templates/overlays/spa-react/frontend/src/pages/HomePage.tsx +72 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/LoginPage.tsx +6 -3
- package/dist/templates/overlays/spa-react/frontend/src/pages/NotFoundPage.tsx +1 -1
- package/dist/templates/package.json +3 -3
- package/dist/templates/src/bootstrap/config.ts +11 -0
- package/dist/templates/src/bootstrap/createApp.ts +2 -1
- package/dist/templates/src/bootstrap/server.ts +1 -1
- package/dist/templates/src/lib/view.ts +2 -2
- package/dist/templates/src/modules/site/index.ts +1 -1
- package/package.json +1 -1
- package/dist/templates/overlays/spa-react/frontend/src/pages/OrganizationsPage.tsx +0 -69
- package/dist/templates/overlays/spa-react/frontend/src/pages/ProjectsPage.tsx +0 -65
- package/dist/templates/overlays/spa-react/frontend/src/pages/TasksPage.tsx +0 -65
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
# API-only scaffold
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This scaffold sets `FRONTEND_MODE=api`: no server-rendered views beyond the welcome page, and no SPA assets.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- No server-rendered views or SPA assets
|
|
7
|
-
- Use bearer tokens or dev auth headers in tests (`actingAs`, `postJson`)
|
|
5
|
+
## What exists after migrating
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- `GET /health` returns plain text `ok`, or `degraded` (503) until the database ping succeeds and the migrated `notes` table exists. Docker HEALTHCHECK uses `/health`.
|
|
8
|
+
- `GET /` returns the welcome page.
|
|
9
|
+
- A `notes` table, with no routes on it yet.
|
|
10
|
+
|
|
11
|
+
Login endpoints depend on the auth layer you chose. Token auth adds `POST /api/v1/auth/login` and `GET /api/v1/auth/me`; JWT auth adds `POST /api/auth/token`; header auth adds none and expects `x-authenticated-user-id` for local work only.
|
|
12
|
+
|
|
13
|
+
There is no CRUD endpoint for `notes`. Adding your own routes is the first thing you do.
|
|
14
|
+
|
|
15
|
+
## Adding a route
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// src/modules/notes/index.ts
|
|
19
|
+
import type { AppModule } from "@getstrata/bootstrap/contracts";
|
|
20
|
+
import { jsonResponse } from "@getstrata/core/http/response";
|
|
21
|
+
import { getSql } from "../../bootstrap/database.ts";
|
|
22
|
+
|
|
23
|
+
const notesModule: AppModule = {
|
|
24
|
+
name: "notes",
|
|
25
|
+
order: 2,
|
|
26
|
+
routes({ kernel }) {
|
|
27
|
+
return {
|
|
28
|
+
"/api/v1/notes": kernel.wrap("api", async () => {
|
|
29
|
+
const rows = await getSql().unsafe<{ id: number; body: string }>(
|
|
30
|
+
"SELECT id, body FROM notes ORDER BY id DESC",
|
|
31
|
+
);
|
|
32
|
+
return jsonResponse({ data: rows });
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default notesModule;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Import from `@getstrata/core/...` subpaths rather than the package root, so singleton state such as the database pool stays shared.
|
|
42
|
+
|
|
43
|
+
## Generating an app instead
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bunx create-strata my-app --frontend api --yes
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A generated app writes its own `docs/API.md` listing only the routes its layers actually serve.
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Link, Navigate, Route, Routes } from "react-router-dom";
|
|
2
2
|
import { useAuth } from "./auth/AuthContext";
|
|
3
|
+
import HomePage from "./pages/HomePage";
|
|
3
4
|
import LoginPage from "./pages/LoginPage";
|
|
4
5
|
import NotFoundPage from "./pages/NotFoundPage";
|
|
5
|
-
import OrganizationsPage from "./pages/OrganizationsPage";
|
|
6
|
-
import ProjectsPage from "./pages/ProjectsPage";
|
|
7
|
-
import TasksPage from "./pages/TasksPage";
|
|
8
6
|
|
|
9
7
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
10
8
|
const { token, loading } = useAuth();
|
|
@@ -28,9 +26,7 @@ function AppShell() {
|
|
|
28
26
|
<header className="site-header">
|
|
29
27
|
<strong>Strata SPA</strong>
|
|
30
28
|
<nav>
|
|
31
|
-
<Link to="/
|
|
32
|
-
<Link to="/projects">Projects</Link>
|
|
33
|
-
<Link to="/tasks">Tasks</Link>
|
|
29
|
+
<Link to="/">Home</Link>
|
|
34
30
|
{user ? (
|
|
35
31
|
<>
|
|
36
32
|
<span className="hint">{user.email}</span>
|
|
@@ -43,29 +39,12 @@ function AppShell() {
|
|
|
43
39
|
</header>
|
|
44
40
|
|
|
45
41
|
<Routes>
|
|
46
|
-
<Route path="/" element={<Navigate to="/organizations" replace />} />
|
|
47
42
|
<Route path="/login" element={<LoginPage />} />
|
|
48
43
|
<Route
|
|
49
|
-
path="/
|
|
44
|
+
path="/"
|
|
50
45
|
element={
|
|
51
46
|
<ProtectedRoute>
|
|
52
|
-
<
|
|
53
|
-
</ProtectedRoute>
|
|
54
|
-
}
|
|
55
|
-
/>
|
|
56
|
-
<Route
|
|
57
|
-
path="/projects"
|
|
58
|
-
element={
|
|
59
|
-
<ProtectedRoute>
|
|
60
|
-
<ProjectsPage />
|
|
61
|
-
</ProtectedRoute>
|
|
62
|
-
}
|
|
63
|
-
/>
|
|
64
|
-
<Route
|
|
65
|
-
path="/tasks"
|
|
66
|
-
element={
|
|
67
|
-
<ProtectedRoute>
|
|
68
|
-
<TasksPage />
|
|
47
|
+
<HomePage />
|
|
69
48
|
</ProtectedRoute>
|
|
70
49
|
}
|
|
71
50
|
/>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useAuth } from "../auth/AuthContext";
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
const { user } = useAuth();
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<section>
|
|
8
|
+
<h1>Signed in</h1>
|
|
9
|
+
<p className="hint">
|
|
10
|
+
This page reads <code>GET /api/v1/auth/me</code> with the bearer token from the login
|
|
11
|
+
response. It is the only resource the generated backend serves, so it is the only one this
|
|
12
|
+
page shows.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div className="card">
|
|
16
|
+
<table>
|
|
17
|
+
<tbody>
|
|
18
|
+
<tr>
|
|
19
|
+
<th>Name</th>
|
|
20
|
+
<td>{user?.name ?? "—"}</td>
|
|
21
|
+
</tr>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Email</th>
|
|
24
|
+
<td>{user?.email ?? "—"}</td>
|
|
25
|
+
</tr>
|
|
26
|
+
<tr>
|
|
27
|
+
<th>Role</th>
|
|
28
|
+
<td>
|
|
29
|
+
<code>{user?.role ?? "—"}</code>
|
|
30
|
+
</td>
|
|
31
|
+
</tr>
|
|
32
|
+
</tbody>
|
|
33
|
+
</table>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<h2>Add your first resource</h2>
|
|
37
|
+
<p>
|
|
38
|
+
Create a module under <code>src/modules/</code> and return routes from it. The kernel picks
|
|
39
|
+
it up on the next boot.
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<pre className="card">
|
|
43
|
+
<code>{`// src/modules/notes/index.ts
|
|
44
|
+
import type { AppModule } from "@getstrata/bootstrap/contracts";
|
|
45
|
+
import { jsonResponse } from "@getstrata/core/http/response";
|
|
46
|
+
import { getSql } from "../../bootstrap/database.ts";
|
|
47
|
+
|
|
48
|
+
const notesModule: AppModule = {
|
|
49
|
+
name: "notes",
|
|
50
|
+
order: 2,
|
|
51
|
+
routes({ kernel }) {
|
|
52
|
+
return {
|
|
53
|
+
"/api/v1/notes": kernel.wrap("api", async () => {
|
|
54
|
+
const rows = await getSql().unsafe<{ id: number; body: string }>(
|
|
55
|
+
"SELECT id, body FROM notes ORDER BY id DESC",
|
|
56
|
+
);
|
|
57
|
+
return jsonResponse({ data: rows });
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default notesModule;`}</code>
|
|
64
|
+
</pre>
|
|
65
|
+
|
|
66
|
+
<p className="hint">
|
|
67
|
+
Then fetch it here with <code>apiFetch("/notes", {"{ token }"})</code>. The{" "}
|
|
68
|
+
<code>notes</code> table already exists after <code>bun run db:migrate</code>.
|
|
69
|
+
</p>
|
|
70
|
+
</section>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -12,7 +12,7 @@ export default function LoginPage() {
|
|
|
12
12
|
const [submitting, setSubmitting] = useState(false);
|
|
13
13
|
|
|
14
14
|
if (token) {
|
|
15
|
-
return <Navigate to="/
|
|
15
|
+
return <Navigate to="/" replace />;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
@@ -22,7 +22,7 @@ export default function LoginPage() {
|
|
|
22
22
|
|
|
23
23
|
try {
|
|
24
24
|
await login(email, password);
|
|
25
|
-
navigate("/
|
|
25
|
+
navigate("/");
|
|
26
26
|
} catch (cause) {
|
|
27
27
|
setError(cause instanceof ApiError ? cause.message : "Login failed");
|
|
28
28
|
} finally {
|
|
@@ -33,7 +33,10 @@ export default function LoginPage() {
|
|
|
33
33
|
return (
|
|
34
34
|
<section className="card">
|
|
35
35
|
<h1>Sign in</h1>
|
|
36
|
-
<p className="hint">
|
|
36
|
+
<p className="hint">
|
|
37
|
+
Seeded users are <code>demo@example.com</code> and <code>admin@example.test</code>, password
|
|
38
|
+
<code>password</code>.
|
|
39
|
+
</p>
|
|
37
40
|
|
|
38
41
|
{error ? <p className="error">{error}</p> : null}
|
|
39
42
|
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"check": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@getstrata/bootstrap": "^1.0.
|
|
15
|
-
"@getstrata/cli": "^1.0.
|
|
16
|
-
"@getstrata/core": "^1.0.
|
|
14
|
+
"@getstrata/bootstrap": "^1.0.2",
|
|
15
|
+
"@getstrata/cli": "^1.0.2",
|
|
16
|
+
"@getstrata/core": "^1.0.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/bun": "^1.4.0",
|
|
@@ -16,3 +16,14 @@ export function loadConfig(): AppConfig {
|
|
|
16
16
|
databaseUrl,
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/** Cookie sessions and signed cookies are keyed by this; there is no default. */
|
|
21
|
+
export function sessionSecret(): string {
|
|
22
|
+
const secret = process.env.SESSION_SECRET?.trim();
|
|
23
|
+
if (!secret) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"SESSION_SECRET is required. Copy .env.example to .env and set it (32+ characters).",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return secret;
|
|
29
|
+
}
|
|
@@ -68,7 +68,8 @@ function createAppContext(): AppContext {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export async function bootstrapApp(options: BootstrapOptions = {}): Promise<BootstrappedApp> {
|
|
71
|
-
const
|
|
71
|
+
const isProduction = process.env.APP_ENV === "production";
|
|
72
|
+
const { migrate: runMigrate = !isProduction } = options;
|
|
72
73
|
|
|
73
74
|
const appConfig = loadConfig();
|
|
74
75
|
const context = createAppContext();
|
|
@@ -9,7 +9,7 @@ const { routes, config } = await bootstrapApp();
|
|
|
9
9
|
|
|
10
10
|
const server = createAppServer(routes, config.port);
|
|
11
11
|
|
|
12
|
-
console.log(
|
|
12
|
+
console.log(`Listening on http://localhost:${server.port} (APP_URL ${config.appUrl})`);
|
|
13
13
|
|
|
14
14
|
if (!(await pingDatabase())) {
|
|
15
15
|
console.warn("Warning: database ping failed.");
|
|
@@ -16,6 +16,6 @@ export async function renderPage(
|
|
|
16
16
|
return htmlResponse(html);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function plainText(body: string): Response {
|
|
20
|
-
return new Response(body, { headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
19
|
+
export function plainText(body: string, status = 200): Response {
|
|
20
|
+
return new Response(body, { status, headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
21
21
|
}
|
|
@@ -19,7 +19,7 @@ const siteModule: AppModule = {
|
|
|
19
19
|
"/health": kernel.wrapWeb(
|
|
20
20
|
withErrorHandling(async () => {
|
|
21
21
|
const dbOk = await pingDatabase();
|
|
22
|
-
return plainText(dbOk ? "ok" : "degraded");
|
|
22
|
+
return plainText(dbOk ? "ok" : "degraded", dbOk ? 200 : 503);
|
|
23
23
|
}),
|
|
24
24
|
),
|
|
25
25
|
};
|
package/package.json
CHANGED
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|