First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.vscode/settings.json
|
||||||
43
docker-compose.yml
Normal file
43
docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
shopping-lists:
|
||||||
|
build: shopping-lists
|
||||||
|
image: shopping-lists
|
||||||
|
restart: "no"
|
||||||
|
volumes:
|
||||||
|
- ./shopping-lists/:/app
|
||||||
|
ports:
|
||||||
|
- 7777:7777
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- flyway
|
||||||
|
env_file:
|
||||||
|
- project.env
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: database-p1-e8774b8e-c7a9-4cce-a779-3b59be02206d
|
||||||
|
image: postgres:14.1
|
||||||
|
restart: "no"
|
||||||
|
env_file:
|
||||||
|
- project.env
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
image: flyway/flyway:8.4.0-alpine
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
volumes:
|
||||||
|
- .:/flyway/sql
|
||||||
|
command: -connectRetries=60 -baselineOnMigrate=true migrate
|
||||||
|
env_file:
|
||||||
|
- project.env
|
||||||
|
|
||||||
|
e2e-playwright:
|
||||||
|
entrypoint: "/bin/true" # Prevent startup on docker-compose up
|
||||||
|
build: e2e-playwright
|
||||||
|
image: e2e-playwright
|
||||||
|
network_mode: host
|
||||||
|
depends_on:
|
||||||
|
- shopping-lists
|
||||||
|
volumes:
|
||||||
|
- ./e2e-playwright/tests:/e2e-playwright/tests
|
||||||
10
e2e-playwright/Dockerfile
Normal file
10
e2e-playwright/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM mcr.microsoft.com/playwright:v1.24.2-focal
|
||||||
|
|
||||||
|
COPY . /e2e-playwright
|
||||||
|
|
||||||
|
WORKDIR /e2e-playwright
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
RUN npx playwright install chrome
|
||||||
|
|
||||||
|
CMD [ "npx", "playwright", "test", "--reporter=list" ]
|
||||||
8
e2e-playwright/package.json
Normal file
8
e2e-playwright/package.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "e2e-playwright-in-docker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "^1.24.2",
|
||||||
|
"@playwright/test": "^1.24.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
e2e-playwright/playwright.config.js
Normal file
20
e2e-playwright/playwright.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
timeout: 10000,
|
||||||
|
retries: 0,
|
||||||
|
reporter: "list",
|
||||||
|
workers: 5,
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:7777",
|
||||||
|
headless: true,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "e2e-headless-chrome",
|
||||||
|
use: {
|
||||||
|
browserName: "chromium",
|
||||||
|
channel: "chrome",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
6
e2e-playwright/tests/hello-world.spec.js
Normal file
6
e2e-playwright/tests/hello-world.spec.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
|
test("Server responds with the text 'Hello world!'", async ({ page }) => {
|
||||||
|
const response = await page.goto("/");
|
||||||
|
expect(await response.text()).toBe("Hello world!");
|
||||||
|
});
|
||||||
12
flyway/sql/V1___initial_schema.sql
Normal file
12
flyway/sql/V1___initial_schema.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE shopping_lists (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
active BOOLEAN DEFAULT TRUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE shopping_list_items (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
shopping_list_id INTEGER REFERENCES shopping_lists(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
collected BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
16
project.env
Normal file
16
project.env
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Database configuration for PostgreSQL (running in container called "database-p1-e8774b8e-c7a9-4cce-a779-3b59be02206d")
|
||||||
|
POSTGRES_USER=username
|
||||||
|
POSTGRES_PASSWORD=password
|
||||||
|
POSTGRES_DB=database
|
||||||
|
|
||||||
|
# Database configuration for Flyway (used for database migrations)
|
||||||
|
FLYWAY_USER=username
|
||||||
|
FLYWAY_PASSWORD=password
|
||||||
|
FLYWAY_URL=jdbc:postgresql://database-p1-e8774b8e-c7a9-4cce-a779-3b59be02206d:5432/database
|
||||||
|
|
||||||
|
# Database configuration for Deno's PostgreSQL driver
|
||||||
|
PGUSER=username
|
||||||
|
PGPASSWORD=password
|
||||||
|
PGHOST=database-p1-e8774b8e-c7a9-4cce-a779-3b59be02206d
|
||||||
|
PGPORT=5432
|
||||||
|
PGDATABASE=database
|
||||||
11
shopping-lists/Dockerfile
Normal file
11
shopping-lists/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM denoland/deno:alpine-1.26.2
|
||||||
|
|
||||||
|
EXPOSE 7777
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN deno cache deps.js
|
||||||
|
|
||||||
|
CMD [ "run", "--unstable", "--watch", "--allow-net", "--allow-read", "--allow-env", "--no-check", "app.js" ]
|
||||||
41
shopping-lists/app.js
Normal file
41
shopping-lists/app.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { serve } from "./deps.js";
|
||||||
|
import { configure } from "./deps.js";
|
||||||
|
import * as listController from "./controllers/listController.js";
|
||||||
|
import * as itemController from "./controllers/itemController.js";
|
||||||
|
import * as mainController from "./controllers/mainController.js";
|
||||||
|
|
||||||
|
configure({
|
||||||
|
views: `${Deno.cwd()}/views/`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRequest = async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname === "/" && request.method === "GET") {
|
||||||
|
return mainController.showMain(request);
|
||||||
|
} else if (url.pathname === "/lists" && request.method === "GET") {
|
||||||
|
return await listController.viewLists(request);
|
||||||
|
} else if (url.pathname === "/lists" && request.method === "POST") {
|
||||||
|
return await listController.addList(request);
|
||||||
|
} else if (url.pathname.match("lists/[0-9]+") && request.method === "GET") {
|
||||||
|
return await listController.viewList(request);
|
||||||
|
} else if (
|
||||||
|
url.pathname.match("lists/[0-9]+/deactivate") &&
|
||||||
|
request.method === "POST"
|
||||||
|
) {
|
||||||
|
return await listController.deactivateList(request);
|
||||||
|
} else if (
|
||||||
|
url.pathname.match("lists/[0-9]+/items/[0-9]+/collect") &&
|
||||||
|
request.method === "POST"
|
||||||
|
) {
|
||||||
|
return await itemController.markItemCollected(request);
|
||||||
|
} else if (
|
||||||
|
url.pathname.match("lists/[0-9]+/items") &&
|
||||||
|
request.method === "POST"
|
||||||
|
) {
|
||||||
|
return await itemController.addItemToList(request);
|
||||||
|
} else {
|
||||||
|
return new Response("Not found", { status: 404 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
serve(handleRequest, { port: 7777 });
|
||||||
22
shopping-lists/controllers/itemController.js
Normal file
22
shopping-lists/controllers/itemController.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as itemService from "../services/itemService.js";
|
||||||
|
import * as requestUtils from "../utils/requestUtils.js";
|
||||||
|
|
||||||
|
const markItemCollected = async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const urlParts = url.pathname.split("/");
|
||||||
|
await itemService.markCollected(urlParts[4], urlParts[2]);
|
||||||
|
|
||||||
|
return requestUtils.redirectTo(`/lists/${urlParts[4]}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addItemToList = async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const urlParts = url.pathname.split("/");
|
||||||
|
const formData = await request.formData();
|
||||||
|
const name = formData.get("name");
|
||||||
|
await itemService.addItem(urlParts[2], name);
|
||||||
|
|
||||||
|
return requestUtils.redirectTo(`/lists/${urlParts[2]}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { markItemCollected, addItemToList };
|
||||||
45
shopping-lists/controllers/listController.js
Normal file
45
shopping-lists/controllers/listController.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { renderFile } from "../deps.js";
|
||||||
|
import * as listService from "../services/listService.js";
|
||||||
|
import * as requestUtils from "../utils/requestUtils.js";
|
||||||
|
|
||||||
|
const responseDetails = {
|
||||||
|
headers: { "Content-Type": "text/html;charset=UTF-8" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const addList = async (request) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const name = formData.get("name");
|
||||||
|
|
||||||
|
await listService.create(name);
|
||||||
|
|
||||||
|
return requestUtils.redirectTo("/lists");
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewList = async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const urlParts = url.pathname.split("/");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
items: await listService.findById(urlParts[2]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(await renderFile("list.eta", data), responseDetails);
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewLists = async (_request) => {
|
||||||
|
const data = {
|
||||||
|
lists: await listService.findAllLists(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(await renderFile("lists.eta", data), responseDetails);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivateList = async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const urlParts = url.pathname.split("/");
|
||||||
|
await listService.deactivateByID(urlParts[2]);
|
||||||
|
|
||||||
|
return requestUtils.redirectTo("/lists");
|
||||||
|
};
|
||||||
|
|
||||||
|
export { addList, viewList, viewLists, deactivateList };
|
||||||
17
shopping-lists/controllers/mainController.js
Normal file
17
shopping-lists/controllers/mainController.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { renderFile } from "../deps.js";
|
||||||
|
import * as mainService from "../services/mainService.js";
|
||||||
|
|
||||||
|
const responseDetails = {
|
||||||
|
headers: { "Content-Type": "text/html;charset=UTF-8" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMain = async (_request) => {
|
||||||
|
const data = {
|
||||||
|
listCount: await mainService.checkList(),
|
||||||
|
itemCount: await mainService.checkItem(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(await renderFile("main.eta", data), responseDetails);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { showMain };
|
||||||
38
shopping-lists/database/database.js
Normal file
38
shopping-lists/database/database.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Pool } from "../deps.js";
|
||||||
|
|
||||||
|
const CONCURRENT_CONNECTIONS = 2;
|
||||||
|
let connectionPool;
|
||||||
|
if (Deno.env.get("DATABASE_URL")) {
|
||||||
|
connectionPool = new Pool(Deno.env.get("DATABASE_URL"), CONCURRENT_CONNECTIONS);
|
||||||
|
} else {
|
||||||
|
connectionPool = new Pool({}, CONCURRENT_CONNECTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeQuery = async (query, params) => {
|
||||||
|
const response = {};
|
||||||
|
let client;
|
||||||
|
|
||||||
|
try {
|
||||||
|
client = await connectionPool.connect();
|
||||||
|
const result = await client.queryObject(query, params);
|
||||||
|
if (result.rows) {
|
||||||
|
response.rows = result.rows;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
response.error = e;
|
||||||
|
} finally {
|
||||||
|
if (client) {
|
||||||
|
try {
|
||||||
|
await client.release();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Unable to release database connection.");
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { executeQuery };
|
||||||
3
shopping-lists/deps.js
Normal file
3
shopping-lists/deps.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { serve } from "https://deno.land/std@0.160.0/http/server.ts";
|
||||||
|
export { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
|
||||||
|
export { configure, renderFile } from "https://deno.land/x/eta@v1.12.3/mod.ts";
|
||||||
38
shopping-lists/fly.toml
Normal file
38
shopping-lists/fly.toml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# fly.toml file generated for shopper on 2022-12-27T19:33:40+02:00
|
||||||
|
|
||||||
|
app = "shopper"
|
||||||
|
kill_signal = "SIGINT"
|
||||||
|
kill_timeout = 5
|
||||||
|
processes = []
|
||||||
|
|
||||||
|
[env]
|
||||||
|
|
||||||
|
[experimental]
|
||||||
|
allowed_public_ports = []
|
||||||
|
auto_rollback = true
|
||||||
|
|
||||||
|
[[services]]
|
||||||
|
http_checks = []
|
||||||
|
internal_port = 7777
|
||||||
|
processes = ["app"]
|
||||||
|
protocol = "tcp"
|
||||||
|
script_checks = []
|
||||||
|
[services.concurrency]
|
||||||
|
hard_limit = 25
|
||||||
|
soft_limit = 20
|
||||||
|
type = "connections"
|
||||||
|
|
||||||
|
[[services.ports]]
|
||||||
|
force_https = true
|
||||||
|
handlers = ["http"]
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
[[services.ports]]
|
||||||
|
handlers = ["tls", "http"]
|
||||||
|
port = 443
|
||||||
|
|
||||||
|
[[services.tcp_checks]]
|
||||||
|
grace_period = "1s"
|
||||||
|
interval = "15s"
|
||||||
|
restart_limit = 0
|
||||||
|
timeout = "2s"
|
||||||
23
shopping-lists/services/itemService.js
Normal file
23
shopping-lists/services/itemService.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { executeQuery } from "../database/database.js";
|
||||||
|
|
||||||
|
const markCollected = async (id, shopping_list_id) => {
|
||||||
|
await executeQuery(
|
||||||
|
"UPDATE shopping_list_items SET collected = true WHERE id = $id AND shopping_list_id = $shopping_list_id;",
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
shopping_list_id: shopping_list_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addItem = async (shopping_list_id, name) => {
|
||||||
|
await executeQuery(
|
||||||
|
"INSERT INTO shopping_list_items (shopping_list_id, name) VALUES ($shopping_list_id, $name);",
|
||||||
|
{
|
||||||
|
shopping_list_id: shopping_list_id,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { markCollected, addItem };
|
||||||
41
shopping-lists/services/listService.js
Normal file
41
shopping-lists/services/listService.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { executeQuery } from "../database/database.js";
|
||||||
|
|
||||||
|
const deactivateByID = async (id) => {
|
||||||
|
await executeQuery(
|
||||||
|
"UPDATE shopping_lists SET active = false WHERE id = $id;",
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (name) => {
|
||||||
|
await executeQuery("INSERT INTO shopping_lists (name) VALUES ($name);", {
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllLists = async () => {
|
||||||
|
const result = await executeQuery(
|
||||||
|
"SELECT * FROM shopping_lists WHERE active = true;"
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = async (shopping_list_id) => {
|
||||||
|
const result = await executeQuery(
|
||||||
|
"SELECT * FROM shopping_list_items WHERE shopping_list_id = $shopping_list_id;",
|
||||||
|
{
|
||||||
|
shopping_list_id: shopping_list_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows && result.rows.length > 0) {
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: 0, name: "Unknown" };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { deactivateByID, create, findAllLists, findById };
|
||||||
15
shopping-lists/services/mainService.js
Normal file
15
shopping-lists/services/mainService.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { executeQuery } from "../database/database.js";
|
||||||
|
|
||||||
|
const checkList = async () => {
|
||||||
|
const result = await executeQuery("SELECT COUNT(*) FROM shopping_lists;");
|
||||||
|
return result.rows[0].count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkItem = async () => {
|
||||||
|
const result = await executeQuery(
|
||||||
|
"SELECT COUNT(*) FROM shopping_list_items;"
|
||||||
|
);
|
||||||
|
return result.rows[0].count;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { checkList, checkItem };
|
||||||
10
shopping-lists/utils/requestUtils.js
Normal file
10
shopping-lists/utils/requestUtils.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const redirectTo = (path) => {
|
||||||
|
return new Response(`Redirecting to ${path}.`, {
|
||||||
|
status: 303,
|
||||||
|
headers: {
|
||||||
|
"Location": path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { redirectTo };
|
||||||
15
shopping-lists/views/layouts/layout.eta
Normal file
15
shopping-lists/views/layouts/layout.eta
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Shared shopping lists</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/papercss@1.8.1/dist/paper.min.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container paper">
|
||||||
|
<%~ it.body %>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
shopping-lists/views/list.eta
Normal file
44
shopping-lists/views/list.eta
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<% layout("./layouts/layout.eta") %>
|
||||||
|
<h1><%= it.list.name %></h1>
|
||||||
|
|
||||||
|
<h2>Add an item</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="/lists/<%= it.list.id %>/items">
|
||||||
|
Name: <input type="text" name="name" />
|
||||||
|
<input type="submit" value="Add!" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Active shopping lists</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Items</th>
|
||||||
|
<th>Collected</th>
|
||||||
|
</tr>
|
||||||
|
<%
|
||||||
|
it.items.sort((a, b) => {
|
||||||
|
if (a.collected === b.collected) {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
return a.collected ? 1 : -1;
|
||||||
|
});
|
||||||
|
it.items.forEach((item) => {
|
||||||
|
%>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<% if (item.collected) { %>
|
||||||
|
<del><%= item.name %></del>
|
||||||
|
<% } else { %>
|
||||||
|
<%= item.name %>
|
||||||
|
<% } %>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<form method="POST" action="/lists/<%= item.shopping_list_id %>/items/<%= item.id %>/collect">
|
||||||
|
<input type="submit" value="Mark collected!" />
|
||||||
|
</form>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<% }); %>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<a href="/lists">Shopping lists</a>
|
||||||
24
shopping-lists/views/lists.eta
Normal file
24
shopping-lists/views/lists.eta
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<% layout("./layouts/layout.eta") %>
|
||||||
|
<h1>Lists</h1>
|
||||||
|
|
||||||
|
<h2>Add a shopping list</h2>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
Name: <input type="text" name="name" />
|
||||||
|
<input type="submit" value="Create!" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Active shopping lists</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<% it.lists.forEach((list) => { %>
|
||||||
|
<li>
|
||||||
|
<a href="/lists/<%= list.id %>"><%= list.name %></a>
|
||||||
|
<form method="POST" action="/lists/<%= list.id %>/deactivate">
|
||||||
|
<input type="submit" value="Deactivate list!" />
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a href="/">Main page</a>
|
||||||
19
shopping-lists/views/main.eta
Normal file
19
shopping-lists/views/main.eta
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<% layout("./layouts/layout.eta") %>
|
||||||
|
<h1>Main page</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<% if (it.listCount !== 0) { %>
|
||||||
|
<li>Shopping lists: <%= it.listCount %></li>
|
||||||
|
<% } else { %>
|
||||||
|
<li>No shopping lists yet.</li>
|
||||||
|
<% } %>
|
||||||
|
<% if (it.itemCount !== 0) { %>
|
||||||
|
<li>Shopping list items: <%= it.itemCount %></li>
|
||||||
|
<% } else { %>
|
||||||
|
<li>No items yet.</li>
|
||||||
|
<% } %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<% if (it.listCount !== 0) { %>
|
||||||
|
<a href="/lists">Lists</a>
|
||||||
|
<% } %>
|
||||||
Reference in New Issue
Block a user