From adf65cdcdb2f0137661d48911c4941d89fb52590 Mon Sep 17 00:00:00 2001 From: Mattes Mohr Date: Sun, 3 Dec 2023 14:27:58 +0100 Subject: [PATCH] Add a feed page --- Public/css/page.css | 4 + .../Administration/FeedAdminController.swift | 105 ++++++++++++++++++ .../Controllers/FeedPageController.swift | 12 +- Sources/Website/Entities/FeedEntity.swift | 34 ++++++ Sources/Website/Localization/de/web.strings | 1 + .../Website/Localization/en-GB/web.strings | 1 + .../Website/Migrations/FeedMigration.swift | 20 ++++ Sources/Website/Models/FeedModel.swift | 36 ++++++ .../PageModels/FeedAdminPageModel.swift | 19 ++++ .../Models/PageModels/FeedPageModel.swift | 1 + .../{ => PageModels}/PrivacyPageModel.swift | 0 .../Website/Repositories/FeedRepository.swift | 59 ++++++++++ Sources/Website/Setup.swift | 2 + .../FeedAdminPage/FeedAdminPage+Form.swift | 64 +++++++++++ .../FeedAdminPage/FeedAdminPage.swift | 77 +++++++++++++ .../FeedAdminPage/Partials/FeedList.swift | 46 ++++++++ .../Areas/Shared/AreaViewContainer.swift | 6 + Sources/Website/Views/FeedPage/FeedPage.swift | 16 +-- 18 files changed, 492 insertions(+), 11 deletions(-) create mode 100644 Sources/Website/Controllers/Areas/Administration/FeedAdminController.swift create mode 100644 Sources/Website/Entities/FeedEntity.swift create mode 100644 Sources/Website/Migrations/FeedMigration.swift create mode 100644 Sources/Website/Models/FeedModel.swift create mode 100644 Sources/Website/Models/PageModels/FeedAdminPageModel.swift rename Sources/Website/Models/{ => PageModels}/PrivacyPageModel.swift (100%) create mode 100644 Sources/Website/Repositories/FeedRepository.swift create mode 100644 Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage+Form.swift create mode 100644 Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage.swift create mode 100644 Sources/Website/Views/Areas/Administration/FeedAdminPage/Partials/FeedList.swift diff --git a/Public/css/page.css b/Public/css/page.css index 679706d..f26f3f3 100644 --- a/Public/css/page.css +++ b/Public/css/page.css @@ -110,3 +110,7 @@ body > footer { .border\:electricblue { --borderColor: 199, 44%, 93%; } + +.grid.ratio\:masonary { + grid-template-rows: masonry; +} diff --git a/Sources/Website/Controllers/Areas/Administration/FeedAdminController.swift b/Sources/Website/Controllers/Areas/Administration/FeedAdminController.swift new file mode 100644 index 0000000..7d5ef4f --- /dev/null +++ b/Sources/Website/Controllers/Areas/Administration/FeedAdminController.swift @@ -0,0 +1,105 @@ +import HTMLKitVapor +import Vapor + +// [/area/admin/feed] +final class FeedAdminController { + + // [/index] + func getIndex(_ request: Request) async throws -> View { + + let page: Int = request.query["page"] ?? 1 + + let pagination = try await FeedRepository(database: request.db) + .find() + .map(FeedModel.Output.init) + .page(page: page, per: 10) + + let viewModel = FeedAdminPageModel.IndexView(pagination: pagination) + + return try await request.htmlkit.render(FeedAdminPage.IndexView(viewModel: viewModel)) + } + + // [/create] + func getCreate(_ request: Request) async throws -> View { + + let viewModel = FeedAdminPageModel.CreateView() + + return try await request.htmlkit.render(FeedAdminPage.CreateView(viewModel: viewModel)) + } + + // [/create/:model] + func postCreate(_ request: Request) async throws -> Response { + + try FeedModel.Input.validate(content: request) + + let model = try request.content.decode(FeedModel.Input.self) + + try await FeedRepository(database: request.db) + .insert(entity: FeedEntity(input: model)) + + return request.redirect(to: "/area/admin/feed/index") + } + + // [/edit/:id] + func getEdit(_ request: Request) async throws -> View { + + guard let id = request.parameters.get("id", as: UUID.self) else { + throw Abort(.badRequest) + } + + guard let entity = try await FeedRepository(database: request.db) + .find(id: id) else { + throw Abort(.notFound) + } + + let viewModel = FeedAdminPageModel.EditView(feed: FeedModel.Output(entity: entity)) + + return try await request.htmlkit.render(FeedAdminPage.EditView(viewModel: viewModel)) + } + + // [/edit/:model] + func postEdit(_ request: Request) async throws -> Response { + + guard let id = request.parameters.get("id", as: UUID.self) else { + throw Abort(.badRequest) + } + + try FeedModel.Input.validate(content: request) + + let model = try request.content.decode(FeedModel.Input.self) + + try await FeedRepository(database: request.db) + .update(entity: FeedEntity(input: model), on: id) + + return request.redirect(to: "/area/admin/feed/index") + } + + // [/delete/:id] + func getDelete(_ request: Request) async throws -> Response { + + guard let id = request.parameters.get("id", as: UUID.self) else { + throw Abort(.badRequest) + } + + try await FeedRepository(database: request.db) + .delete(id: id) + + return request.redirect(to: "/area/admin/feed/index") + } +} + +extension FeedAdminController: RouteCollection { + + func boot(routes: RoutesBuilder) throws { + + routes.group("feed") { routes in + + routes.get("index", use: self.getIndex) + routes.get("create", use: self.getCreate) + routes.post("create", use: self.postCreate) + routes.get("edit", ":id", use: self.getEdit) + routes.post("edit", ":id", use: self.postEdit) + routes.get("delete", ":id", use: self.getDelete) + } + } +} diff --git a/Sources/Website/Controllers/FeedPageController.swift b/Sources/Website/Controllers/FeedPageController.swift index 2284a3e..755b1e3 100644 --- a/Sources/Website/Controllers/FeedPageController.swift +++ b/Sources/Website/Controllers/FeedPageController.swift @@ -6,7 +6,17 @@ final class FeedPageController { // [/index] func getIndex(_ request: Request) async throws -> View { - return try await request.htmlkit.render(FeedPage.IndexView(viewModel: .init())) + + let page: Int = request.query["page"] ?? 1 + + let feeds = try await FeedRepository(database: request.db) + .find() + .map(FeedModel.Output.init) + .page(page: page, per: 10) + + let viewModel = FeedPageModel.IndexView(pagination: feeds) + + return try await request.htmlkit.render(FeedPage.IndexView(viewModel: viewModel)) } } diff --git a/Sources/Website/Entities/FeedEntity.swift b/Sources/Website/Entities/FeedEntity.swift new file mode 100644 index 0000000..b0c7ab5 --- /dev/null +++ b/Sources/Website/Entities/FeedEntity.swift @@ -0,0 +1,34 @@ +import Fluent +import Foundation + +final class FeedEntity: Model { + + static let schema = "feeds" + + @ID(key: "id") + var id: UUID? + + @Field(key: "message") + var message: String + + @Timestamp(key: "created_at", on: .create) + var createdAt: Date? + + @Timestamp(key: "modified_at", on: .update) + var modifiedAt: Date? + + init() {} + + init(id: UUID? = nil, message: String, createdAt: Date? = nil, modifiedAt: Date? = nil) { + + self.id = id + self.message = message + self.createdAt = createdAt + self.modifiedAt = modifiedAt + } + + convenience init(input: FeedModel.Input) { + + self.init(message: input.message) + } +} diff --git a/Sources/Website/Localization/de/web.strings b/Sources/Website/Localization/de/web.strings index 3572df8..42d0bd5 100644 --- a/Sources/Website/Localization/de/web.strings +++ b/Sources/Website/Localization/de/web.strings @@ -4,6 +4,7 @@ "menu.articles" = "Beiträge"; "menu.assets" = "Mediathek"; "menu.reports" = "Berichte"; +"menu.feed" = "Texte"; "menu.users" = "Benutzer"; "menu.legal" = "Impressum"; "menu.privacy" = "Datenschutz"; diff --git a/Sources/Website/Localization/en-GB/web.strings b/Sources/Website/Localization/en-GB/web.strings index 7d1932f..0edd0de 100644 --- a/Sources/Website/Localization/en-GB/web.strings +++ b/Sources/Website/Localization/en-GB/web.strings @@ -4,6 +4,7 @@ "menu.articles" = "Articles"; "menu.assets" = "Assets"; "menu.reports" = "Reports"; +"menu.feed" = "Feed"; "menu.users" = "Users"; "menu.legal" = "Legal"; "menu.privacy" = "Privacy"; diff --git a/Sources/Website/Migrations/FeedMigration.swift b/Sources/Website/Migrations/FeedMigration.swift new file mode 100644 index 0000000..a2cf983 --- /dev/null +++ b/Sources/Website/Migrations/FeedMigration.swift @@ -0,0 +1,20 @@ +import Fluent + +struct FeedMigration: AsyncMigration { + + func prepare(on database: Database) async throws { + + try await database.schema("feeds") + .id() + .field("message", .string, .required) + .field("created_at", .datetime) + .field("modified_at", .datetime) + .create() + } + + func revert(on database: Database) async throws { + + try await database.schema("feeds") + .delete() + } +} diff --git a/Sources/Website/Models/FeedModel.swift b/Sources/Website/Models/FeedModel.swift new file mode 100644 index 0000000..a3963a8 --- /dev/null +++ b/Sources/Website/Models/FeedModel.swift @@ -0,0 +1,36 @@ +import Vapor +import HTMLKitComponents + +struct FeedModel { + + struct Input: Content, Validatable { + + var message: String + + static func validations(_ validations: inout Validations) { + + validations.add("message", as: String.self, is: !.empty) + } + + static let validators = [ + Validator(field: "message", rule: .value) + ] + } + + struct Output: Content { + + var id: UUID + var message: String + + init(id: UUID, message: String) { + + self.id = id + self.message = message + } + + init(entity: FeedEntity) { + + self.init(id: entity.id!, message: entity.message) + } + } +} diff --git a/Sources/Website/Models/PageModels/FeedAdminPageModel.swift b/Sources/Website/Models/PageModels/FeedAdminPageModel.swift new file mode 100644 index 0000000..0160e19 --- /dev/null +++ b/Sources/Website/Models/PageModels/FeedAdminPageModel.swift @@ -0,0 +1,19 @@ +import HTMLKitComponents + +enum FeedAdminPageModel { + + struct IndexView { + var title: String = "Show feed" + let pagination: Pagination<[FeedModel.Output]> + } + + struct CreateView { + var title: String = "Create feed" + } + + struct EditView { + + var title: String = "Edit feed" + let feed: FeedModel.Output + } +} diff --git a/Sources/Website/Models/PageModels/FeedPageModel.swift b/Sources/Website/Models/PageModels/FeedPageModel.swift index 31854a5..fa6fc88 100644 --- a/Sources/Website/Models/PageModels/FeedPageModel.swift +++ b/Sources/Website/Models/PageModels/FeedPageModel.swift @@ -5,5 +5,6 @@ enum FeedPageModel { struct IndexView { var title: String = "Feed" + let pagination: Pagination<[FeedModel.Output]> } } diff --git a/Sources/Website/Models/PrivacyPageModel.swift b/Sources/Website/Models/PageModels/PrivacyPageModel.swift similarity index 100% rename from Sources/Website/Models/PrivacyPageModel.swift rename to Sources/Website/Models/PageModels/PrivacyPageModel.swift diff --git a/Sources/Website/Repositories/FeedRepository.swift b/Sources/Website/Repositories/FeedRepository.swift new file mode 100644 index 0000000..4966991 --- /dev/null +++ b/Sources/Website/Repositories/FeedRepository.swift @@ -0,0 +1,59 @@ +import Fluent +import Foundation + +final class FeedRepository { + + let database: Database + + init(database: Database) { + + self.database = database + } + + func find(id: UUID) async throws -> FeedEntity? { + + return try await FeedEntity.query(on: database) + .filter(\.$id == id) + .first() + } + + func find() async throws -> [FeedEntity] { + + return try await FeedEntity.query(on: database) + .sort(\.$modifiedAt, .descending) + .all() + } + + func insert(entity: FeedEntity) async throws { + try await entity.create(on: database) + } + + func patch(field: KeyPath, to value: Field.Value, on id: UUID) async throws where Field.Model == FeedEntity { + + try await FeedEntity.query(on: database) + .filter(\.$id == id) + .set(field, to: value) + .update() + } + + func update(entity: FeedEntity, on id: UUID) async throws { + + try await FeedEntity.query(on: database) + .filter(\.$id == id) + .set(\.$message, to: entity.message) + .update() + } + + func delete(id: UUID) async throws { + + try await FeedEntity.query(on: database) + .filter(\.$id == id) + .delete() + } + + func count() async throws -> Int { + + return try await FeedEntity.query(on: database) + .count() + } +} diff --git a/Sources/Website/Setup.swift b/Sources/Website/Setup.swift index 72f6853..72da292 100644 --- a/Sources/Website/Setup.swift +++ b/Sources/Website/Setup.swift @@ -63,6 +63,7 @@ struct Setup { try group.register(collection: ArticleAdminController()) try group.register(collection: AssetAdminController()) try group.register(collection: UserAdminController()) + try group.register(collection: FeedAdminController()) try group.register(collection: ReportAdminController()) } } @@ -94,6 +95,7 @@ struct Setup { application.migrations.add(ContactMigration()) application.migrations.add(ReportMigration()) application.migrations.add(LinkMigration()) + application.migrations.add(FeedMigration()) try await application.autoMigrate() } diff --git a/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage+Form.swift b/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage+Form.swift new file mode 100644 index 0000000..6727aab --- /dev/null +++ b/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage+Form.swift @@ -0,0 +1,64 @@ +import HTMLKit +import HTMLKitComponents + +extension FeedAdminPage { + + struct CreateForm: View { + + var body: Content { + Form(method: .post) { + VStack { + FieldLabel(for: "message") { + "Message" + } + TextEditor(name: "message") { + } + .borderShape(.smallrounded) + .lineLimit(8) + } + HStack { + Button(role: .submit) { + "Submit" + } + .buttonStyle(.primary) + .borderShape(.smallrounded) + } + } + .tag("create-form") + .onSubmit { form in + form.validate("create-form", FeedModel.Input.validators) + } + } + } + + struct EditForm: View { + + var feed: FeedModel.Output + + var body: Content { + Form(method: .post) { + VStack { + FieldLabel(for: "message") { + "Message" + } + TextEditor(name: "message") { + feed.message + } + .borderShape(.smallrounded) + .lineLimit(8) + } + HStack { + Button(role: .submit) { + "Submit" + } + .buttonStyle(.primary) + .borderShape(.smallrounded) + } + } + .tag("edit-form") + .onSubmit { form in + form.validate("edit-form", FeedModel.Input.validators) + } + } + } +} diff --git a/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage.swift b/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage.swift new file mode 100644 index 0000000..7c3f685 --- /dev/null +++ b/Sources/Website/Views/Areas/Administration/FeedAdminPage/FeedAdminPage.swift @@ -0,0 +1,77 @@ +import HTMLKit +import HTMLKitComponents + +enum FeedAdminPage { + + struct IndexView: View { + + var viewModel: FeedAdminPageModel.IndexView + + var body: Content { + AreaViewContainer { + Header { + HStack { + Text { + viewModel.title + } + .fontSize(.medium) + LinkButton(destination: "/area/admin/feed/create") { + Text { + "Create" + } + } + .buttonStyle(.primary) + .borderShape(.smallrounded) + } + } + Section { + FeedList(feeds: viewModel.pagination.items) + HStack { + PagePagination(meta: viewModel.pagination.meta) + } + .contentSpace(.between) + } + } + } + } + + struct CreateView: View { + + var viewModel: FeedAdminPageModel.CreateView + + var body: Content { + AreaViewContainer { + Header { + Text { + viewModel.title + } + .fontSize(.medium) + .fontWeight(.medium) + } + Section { + FeedAdminPage.CreateForm() + } + } + } + } + + struct EditView: View { + + var viewModel: FeedAdminPageModel.EditView + + var body: Content { + AreaViewContainer { + Header { + Text { + viewModel.title + } + .fontSize(.medium) + .fontWeight(.medium) + } + Section { + FeedAdminPage.EditForm(feed: viewModel.feed) + } + } + } + } +} diff --git a/Sources/Website/Views/Areas/Administration/FeedAdminPage/Partials/FeedList.swift b/Sources/Website/Views/Areas/Administration/FeedAdminPage/Partials/FeedList.swift new file mode 100644 index 0000000..99d5bfc --- /dev/null +++ b/Sources/Website/Views/Areas/Administration/FeedAdminPage/Partials/FeedList.swift @@ -0,0 +1,46 @@ +import HTMLKit +import HTMLKitComponents + +struct FeedList: View { + + var feeds: [FeedModel.Output] + + var body: Content { + Card { + List(direction: .vertical) { + for feed in feeds { + ListRow { + HStack { + Text { + feed.message + } + .frame(width: .ten) + Dropdown { + List(direction: .vertical) { + ListRow { + Link(destination: "/area/admin/feed/edit/\(feed.id)") { + Symbol(system: "folder") + Text { + "Edit" + } + } + } + } + } label: { + Text { + "\u{2981}\u{2981}\u{2981}" + } + } + .frame(width: .two) + .borderShape(.smallrounded) + } + } + .padding(insets: .vertical, length: .small) + } + } + .listStyle(.listgroup) + } + .borderShape(.smallrounded) + .margin(insets: .bottom, length: .medium) + } +} diff --git a/Sources/Website/Views/Areas/Shared/AreaViewContainer.swift b/Sources/Website/Views/Areas/Shared/AreaViewContainer.swift index 540d4df..f0be8f3 100644 --- a/Sources/Website/Views/Areas/Shared/AreaViewContainer.swift +++ b/Sources/Website/Views/Areas/Shared/AreaViewContainer.swift @@ -48,6 +48,12 @@ struct AreaViewContainer: View { Text("menu.articles") } } + ListRow { + Link(destination: "/area/admin/feed/index") { + Symbol(system: "photo") + Text("menu.feed") + } + } ListRow { Link(destination: "/area/admin/assets/index") { Symbol(system: "photo") diff --git a/Sources/Website/Views/FeedPage/FeedPage.swift b/Sources/Website/Views/FeedPage/FeedPage.swift index df6ba19..60ea475 100644 --- a/Sources/Website/Views/FeedPage/FeedPage.swift +++ b/Sources/Website/Views/FeedPage/FeedPage.swift @@ -16,16 +16,12 @@ enum FeedPage { .font(.subheadline) } Section { - Grid(ratio: .sixth) { - Thumbnail { - } - Thumbnail { - } - Thumbnail { - } - Thumbnail { - } - Thumbnail { + Grid(ratio: .custom("masonary")) { + for feed in viewModel.pagination.items { + Card { + feed.message + } + .borderShape(.smallrounded) } } .contentSpace(.small)