feat: Initial commit

This commit is contained in:
2024-12-31 17:02:29 -05:00
parent bb568ba60e
commit 8dba393267
21 changed files with 1881 additions and 63 deletions

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
root = true
[*.swift]
indent_style = space
indent_size = 2
tab_width = 2
trim_trailing_whitespace = true

View File

@@ -0,0 +1 @@
{"projectFile": "/Volumes/Bucket/Repos/hello/Package.swift", "scheme": "hello", "devices": [{"id": "00008103-000E79D011EA001E", "name": "My Mac", "arch": "arm64e", "platform": "macOS"}]}

View File

@@ -0,0 +1 @@
{"deviceName": "My Mac", "workingDirectory": "/Volumes/Bucket/Repos/hello", "swiftPackage": "/Volumes/Bucket/Repos/hello/Package.swift", "platform": "macOS", "destination": "00008103-000E79D011EA001E", "scheme": "hello"}

11
.swiftformat Normal file
View File

@@ -0,0 +1,11 @@
--self init-only
--indent 2
--ifdef indent
--trimwhitespace always
--wraparguments before-first
--wrapparameters before-first
--wrapcollections preserve
--wrapconditions after-first
--typeblanklines preserve
--commas inline
--stripunusedargs closure-only

11
.swiftlint.yml Normal file
View File

@@ -0,0 +1,11 @@
disabled_rules:
- closing_brace
- fuction_body_length
- opening_brace
- nesting
included:
- Sources
- Tests
ignore_multiline_statement_conditions: true

294
Package.resolved Normal file
View File

@@ -0,0 +1,294 @@
{
"originHash" : "f7e5f899f6a75ee8db21a1bb07e44cbbdf4d7aaafddefc76f56bd4f3c446abd7",
"pins" : [
{
"identity" : "async-http-client",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swift-server/async-http-client.git",
"state" : {
"revision" : "2119f0d9cc1b334e25447fe43d3693c0e60e6234",
"version" : "1.24.0"
}
},
{
"identity" : "async-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/async-kit.git",
"state" : {
"revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31",
"version" : "1.20.0"
}
},
{
"identity" : "console-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/console-kit.git",
"state" : {
"revision" : "966d89ae64cd71c652a1e981bc971de59d64f13d",
"version" : "4.15.1"
}
},
{
"identity" : "fluent",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/fluent.git",
"state" : {
"revision" : "223b27d04ab2b51c25503c9922eecbcdf6c12f89",
"version" : "4.12.0"
}
},
{
"identity" : "fluent-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/fluent-kit.git",
"state" : {
"revision" : "614d3ec27cdef50cfb9fc3cfd382b6a4d9578cff",
"version" : "1.49.0"
}
},
{
"identity" : "fluent-sqlite-driver",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/fluent-sqlite-driver.git",
"state" : {
"revision" : "6e3a5ff7f2cb733771a6bd71dd3a491cce79f24d",
"version" : "4.8.0"
}
},
{
"identity" : "leaf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/leaf.git",
"state" : {
"revision" : "bf48d2423c00292b5937c60166c7db99705cae47",
"version" : "4.4.1"
}
},
{
"identity" : "leaf-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/leaf-kit.git",
"state" : {
"revision" : "d0ca4417166ef7868d28ad21bc77d36b8735a0fc",
"version" : "1.11.1"
}
},
{
"identity" : "multipart-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/multipart-kit.git",
"state" : {
"revision" : "3498e60218e6003894ff95192d756e238c01f44e",
"version" : "4.7.1"
}
},
{
"identity" : "routing-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/routing-kit.git",
"state" : {
"revision" : "8c9a227476555c55837e569be71944e02a056b72",
"version" : "4.9.1"
}
},
{
"identity" : "sql-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/sql-kit.git",
"state" : {
"revision" : "e0b35ff07601465dd9f3af19a1c23083acaae3bd",
"version" : "3.32.0"
}
},
{
"identity" : "sqlite-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/sqlite-kit.git",
"state" : {
"revision" : "f35a863ecc2da5d563b836a9a696b148b0f4169f",
"version" : "4.5.2"
}
},
{
"identity" : "sqlite-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/sqlite-nio.git",
"state" : {
"revision" : "0c6a711c9779b5493364631e4f014618ef12a40a",
"version" : "1.10.5"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-algorithms.git",
"state" : {
"revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42",
"version" : "1.2.0"
}
},
{
"identity" : "swift-asn1",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-asn1.git",
"state" : {
"revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6",
"version" : "1.3.0"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
"version" : "1.1.4"
}
},
{
"identity" : "swift-crypto",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto.git",
"state" : {
"revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779",
"version" : "3.10.0"
}
},
{
"identity" : "swift-distributed-tracing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-distributed-tracing.git",
"state" : {
"revision" : "6483d340853a944c96dbcc28b27dd10b6c581703",
"version" : "1.1.2"
}
},
{
"identity" : "swift-http-types",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-types",
"state" : {
"revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3",
"version" : "1.3.1"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91",
"version" : "1.6.2"
}
},
{
"identity" : "swift-metrics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-metrics.git",
"state" : {
"revision" : "e0165b53d49b413dd987526b641e05e246782685",
"version" : "2.5.0"
}
},
{
"identity" : "swift-nio",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34",
"version" : "2.77.0"
}
},
{
"identity" : "swift-nio-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6",
"version" : "1.24.1"
}
},
{
"identity" : "swift-nio-http2",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "170f4ca06b6a9c57b811293cebcb96e81b661310",
"version" : "1.35.0"
}
},
{
"identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "c7e95421334b1068490b5d41314a50e70bab23d1",
"version" : "2.29.0"
}
},
{
"identity" : "swift-nio-transport-services",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214",
"version" : "1.23.0"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics.git",
"state" : {
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
"version" : "1.0.2"
}
},
{
"identity" : "swift-service-context",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-service-context.git",
"state" : {
"revision" : "0c62c5b4601d6c125050b5c3a97f20cce881d32b",
"version" : "1.1.0"
}
},
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "c8a44d836fe7913603e246acab7c528c2e780168",
"version" : "1.4.0"
}
},
{
"identity" : "vapor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/vapor.git",
"state" : {
"revision" : "4d7456c0d4b33ef82783a90ecfeae33a52a3972a",
"version" : "4.111.0"
}
},
{
"identity" : "websocket-kit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/vapor/websocket-kit.git",
"state" : {
"revision" : "4232d34efa49f633ba61afde365d3896fc7f8740",
"version" : "2.15.0"
}
}
],
"version" : 3
}

View File

@@ -2,39 +2,45 @@
import PackageDescription
let package = Package(
name: "hello",
platforms: [
.macOS(.v13)
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "AppTests",
dependencies: [
.target(name: "App"),
.product(name: "VaporTesting", package: "vapor"),
],
swiftSettings: swiftSettings
)
],
swiftLanguageModes: [.v5]
name: "hello",
platforms: [
.macOS(.v13)
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/leaf.git", from: "4.4.0")
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Fluent", package: "fluent"),
.product(name: "Leaf", package: "leaf")
],
swiftSettings: swiftSettings
),
.testTarget(
name: "AppTests",
dependencies: [
.target(name: "App"),
.product(name: "VaporTesting", package: "vapor")
],
swiftSettings: swiftSettings
)
],
swiftLanguageModes: [.v5]
)
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableExperimentalFeature("StrictConcurrency"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableExperimentalFeature("StrictConcurrency")
] }

35
Public/js/site.js Normal file
View File

@@ -0,0 +1,35 @@
function buildJsonFormData(form) {
const jsonFormData = {};
for (const pair of new FormData(form)) {
jsonFormData[pair[0]] = pair[1];
}
return jsonFormData
}
async function postLoginForm(json) {
try {
const response = await fetch("api/user", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(json)
});
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const jsonData = await response.json();
console.log(jsonData);
} catch (error) {
console.error(error.message);
}
}
async function submitLoginForm() {
const loginForm = document.querySelector("#loginForm");
if (loginForm) {
const jsonData = buildJsonFormData(loginForm);
console.log(JSON.stringify(jsonData));
await postLoginForm(jsonData);
}
}

95
Public/styles/site.css Normal file
View File

@@ -0,0 +1,95 @@
body {
background-color: #1E1E2E;
color: white;
}
p {
font-family: verdana;
font-size: 20px;
}
h1 {
color: violet;
}
h2 {
color: violet;
}
input[type=text] {
border 2px solid red;
border-radius 4px;
margin: 8px 0;
padding: 12px 20px;
width: 50%;
box-sizing: border-box;
}
select {
border 2px solid violet;
background-color: blue;
border-radius: 10px;
color: white;
margin: 10px;
padding: 10px 40px;
font-size: 20px;
}
label {
padding: 10px;
margin: 10px;
}
li {
font-size: 20px;
}
.pros {
border 2px solid green;
border-radius: 10px;
background-color: MediumSeaGreen;
margin: 10px;
opacity: 0.7;
}
.cons {
border 2px solid green;
border-radius: 10px;
background-color: Tomato;
margin: 10px;
opacity: 0.7;
}
.listHeader {
margin: auto;
padding: 20px;
}
.center {
margin: auto;
top: 50%;
width: 100%;
text-align: center;
}
#loginForm {
position: relative;
padding 40px;
margin 80px;
}
form {
position: relative;
padding: 40px;
}
.loginButton {
background-color: blue;
color: white;
border: 2px solid violet;
border-radius: 20px;
height 100px;
width: 50%;
padding: 10px;
font-size: 16px;
}

View File

@@ -0,0 +1,43 @@
#extend("main"):
#export("body"):
<div class="center">
<h1>Welcome #capitalized(name)</h1>
<p>
Please add your pro's and cons during the talk to the list below.
</p>
<p>
<small>You can add as many pros and cons as you would like.</small>
</p>
<form id="proconForm" action="submitProOrCon">
<select id="type", name="type">
<option value="pro">Pro</option>
<option value="con">Con</option>
</select>
<br>
<br>
<label for="description">Description:</label>
<br>
<input type="text" id="description" name="description" required>
<br>
<br>
<input class="loginButton" type="submit" value="Submit">
</form>
</div>
<div class="pros">
<p class="listHeader">Pros - <small>Count: #count(pros)</small></p>
<ul>
#for(item in pros):
<li>#(item.description)</li>
#endfor
</ul>
</div>
<div class="cons">
<p class="listHeader">Cons - <small>Count: #count(cons)</small></p>
<ul>
#for(item in cons):
<li>#(item.description)</li>
#endfor
</ul>
</div>
#endexport
#endextend

View File

@@ -0,0 +1,16 @@
#extend("main"):
#export("body"):
<div class="center">
<h1>Welcome to chiller Pro vs. Cons</h1>
<p style="font-size: 12px;">Enter your display name below to get started!</p>
<form id="loginForm" action="login">
<label for="displayName">Display Name:</label>
<br>
<input type="text" id="displayName" name="displayName" placeholder="Name" required>
<br>
<br>
<input type="submit" value="Submit" class="loginButton">
</form>
</div>
#endexport
#endextend

15
Resources/Views/main.leaf Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Chiller Pro vs. Cons</title>
<link rel="stylesheet" href="styles/site.css">
<script src="js/site.js"></script>
</head>
<body>
<header><h1>Chiller Pro vs. Cons<h1></header>
#import("body")
<footer>
<span>2025 Symposium</span>
</footer>
</body>
</html>

View File

@@ -0,0 +1,49 @@
import Fluent
import Foundation
import Vapor
struct ApiController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
let users = routes.grouped("api", "users")
users.get(use: usersIndex(req:))
users.post(use: createUser(req:))
let proCon = routes.grouped("api", "procons")
proCon.get(use: prosAndConsIndex(req:))
proCon.post(use: createProCon(req:))
}
@Sendable
func usersIndex(req: Request) async throws -> [User] {
try await User.query(on: req.db).all()
}
@Sendable
func prosAndConsIndex(req: Request) async throws -> [ProCon] {
try await ProCon.query(on: req.db).all()
}
@Sendable
func createUser(req: Request) async throws -> User {
let user = try req.content.decode(User.self)
try await user.save(on: req.db)
return user
}
@Sendable
func createProCon(req: Request) async throws -> ProCon {
let proconData = try req.content.decode(ProConDTO.self)
let proCon = ProCon(type: proconData.type, description: proconData.description, userId: proconData.userId)
try await proCon.create(on: req.db)
return proCon
}
}
struct ProConDTO: Content {
let id: UUID?
let type: ProCon.ProConType
let description: String
let userId: User.IDValue
}

View File

@@ -0,0 +1,19 @@
import Fluent
import Vapor
struct CreateProCon: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("procon")
.id()
.field("description", .string, .required)
.field("type", .string, .required)
.field("userId", .uuid, .required, .references("user", "id"))
.create()
}
func revert(on database: Database) async throws {
try await database.schema("procon").delete()
}
}

View File

@@ -0,0 +1,16 @@
import Fluent
import Vapor
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("user")
.id()
.field("displayName", .string, .required)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("user").delete()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
import Fluent
import Foundation
import Vapor
final class ProCon: Model {
static let schema = "procon"
@ID(key: .id)
var id: UUID?
@Parent(key: "userId")
var user: User
@Field(key: "description")
var description: String
@Field(key: "type")
var type: ProConType
init() {}
init(
id: UUID? = nil,
type: ProConType,
description: String,
userId: User.IDValue
) {
self.id = id
self.type = type
self.description = description
$user.id = userId
}
enum ProConType: String, Codable, Equatable, Sendable {
case pro, con
}
}
extension ProCon: Content {}

View File

@@ -0,0 +1,25 @@
import Fluent
import Foundation
import Vapor
final class User: Model {
static let schema = "user"
@ID(key: .id)
var id: UUID?
@Field(key: "displayName")
var displayName: String
@Children(for: \.$user)
var prosAndCons: [ProCon]
init() {}
init(id: UUID? = nil, displayName: String) {
self.id = id
self.displayName = displayName
}
}
extension User: Content {}

View File

@@ -1,9 +1,22 @@
import Fluent
import FluentSQLiteDriver
import Leaf
import Vapor
// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
// register routes
try routes(app)
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.middleware.use(app.sessions.middleware)
// app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
app.databases.use(.sqlite(.memory), as: .sqlite)
app.migrations.add(CreateProCon())
app.migrations.add(CreateUser())
app.views.use(.leaf)
// register routes
try routes(app)
try app.register(collection: ApiController())
try await app.autoMigrate()
}

View File

@@ -1,31 +1,31 @@
import Vapor
import Logging
import NIOCore
import NIOPosix
import Vapor
@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = try await Application.make(env)
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
// This attempts to install NIO as the Swift Concurrency global executor.
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
do {
try await configure(app)
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()
throw error
}
try await app.execute()
try await app.asyncShutdown()
let app = try await Application.make(env)
// This attempts to install NIO as the Swift Concurrency global executor.
// You can enable it if you'd like to reduce the amount of context switching between NIO and Swift Concurrency.
// Note: this has caused issues with some libraries that use `.wait()` and cleanly shutting down.
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
do {
try await configure(app)
} catch {
app.logger.report(error: error)
try? await app.asyncShutdown()
throw error
}
try await app.execute()
try await app.asyncShutdown()
}
}

View File

@@ -1,11 +1,93 @@
import Fluent
import Foundation
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
app.get { req in
let output = try await req.view.render("login")
return output
}
app.get("loggedIn") { req in
guard let userIdString = req.session.data["userId"],
let displayName = req.session.data["displayName"],
let userId = UUID(uuidString: userIdString)
else {
return try await req.view.render("/")
}
guard let user = try await User.query(on: req.db)
.filter(\.$id == userId)
.with(\.$prosAndCons)
.first()
else {
throw Abort(.unauthorized)
}
// let prosAndCons = try await user.$prosAndCons.get(on: req.db)
return try await req.view.render(
"loggedIn",
LoggedInContext(name: displayName, prosAndCons: user.prosAndCons)
)
}
app.get("submitProOrCon") { req in
let params = try req.query.decode(SubmitProOrCon.self)
guard let userIdString = req.session.data["userId"],
let userId = UUID(uuidString: userIdString)
else {
throw Abort(.unauthorized)
}
let proOrCon = ProCon(type: params.type, description: params.description, userId: userId)
_ = try await req.db.transaction {
proOrCon.save(on: $0)
}
.get()
return req.redirect(to: "loggedIn")
}
app.get("login") { req in
let params = try req.query.decode(LoginParams.self)
req.logger.info("params: \(params)")
do {
try checkForBadWords(in: params.displayName)
} catch {
throw Abort(.unauthorized, reason: "Stop using such naughty language.")
}
app.get("hello") { req async -> String in
"Hello, world!"
}
let user = User(displayName: params.displayName)
_ = try await req.db.transaction {
user.save(on: $0)
}.get()
let userId = user.id?.uuidString ?? "nil"
req.session.data["userId"] = userId
req.session.data["displayName"] = user.displayName
// return try await req.view.render("loggedIn", ["name": user.displayName])
return req.redirect(to: "loggedIn")
}
}
struct DisplayNameError: Error {}
struct LoginParams: Content {
let displayName: String
}
struct SubmitProOrCon: Content {
let type: ProCon.ProConType
let description: String
}
struct LoggedInContext: Encodable {
let name: String
let pros: [ProCon]
let cons: [ProCon]
init(name: String, prosAndCons: [ProCon]) {
self.name = name
self.cons = prosAndCons.filter { $0.type == .con }
self.pros = prosAndCons.filter { $0.type == .pro }
}
}