From 82ce3fdf03a654d53ddaf3babfe1bbec70b87efe Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 13 Mar 2019 13:52:02 +0200 Subject: [PATCH] Implement Feed admin form --- .../DatetimeWidget/DatetimeWidget.tsx | 41 ++++ src/components/DatetimeWidget/index.ts | 2 + src/models/Event.ts | 7 +- src/models/Feed.ts | 48 +++- src/pages/AdminFeedPage/AdminFeedPage.tsx | 6 +- src/pages/EventCreatePage/EventCreatePage.tsx | 27 +-- src/pages/FeedCreatePage/FeedCreatePage.scss | 65 +++++ src/pages/FeedCreatePage/FeedCreatePage.tsx | 228 ++++++++++++++++++ src/pages/FeedCreatePage/index.ts | 2 + src/pages/FrontPage/FrontPage.tsx | 6 +- src/routes.tsx | 3 + 11 files changed, 404 insertions(+), 31 deletions(-) create mode 100644 src/components/DatetimeWidget/DatetimeWidget.tsx create mode 100644 src/components/DatetimeWidget/index.ts create mode 100644 src/pages/FeedCreatePage/FeedCreatePage.scss create mode 100644 src/pages/FeedCreatePage/FeedCreatePage.tsx create mode 100644 src/pages/FeedCreatePage/index.ts diff --git a/src/components/DatetimeWidget/DatetimeWidget.tsx b/src/components/DatetimeWidget/DatetimeWidget.tsx new file mode 100644 index 0000000..51c79d6 --- /dev/null +++ b/src/components/DatetimeWidget/DatetimeWidget.tsx @@ -0,0 +1,41 @@ +import * as React from "react"; + +export interface DatetimeWidgetProps { + value: string; + onChange: (value: string) => void; + required: boolean; + disabled: boolean; +} +export interface DatetimeWidgetState {} + +class DatetimeWidget extends React.Component { + render() { + const { value, onChange, required, disabled } = this.props; + + let date, time; + if (value && value.length !== 0) { + let rest; + [date, rest] = value.split("T"); + time = rest.slice(0, 5); + } + + return ( +
+ onChange(`${event.target.value}T${time}`)} + required={required} + disabled={disabled} + value={date} /> + onChange(`${date}T${event.target.value}:00`)} + required={required} + disabled={disabled} + value={time} /> +
+ ); + } +} + +export default DatetimeWidget; diff --git a/src/components/DatetimeWidget/index.ts b/src/components/DatetimeWidget/index.ts new file mode 100644 index 0000000..396b80b --- /dev/null +++ b/src/components/DatetimeWidget/index.ts @@ -0,0 +1,2 @@ +import DatetimeWidget from "./DatetimeWidget"; +export default DatetimeWidget; diff --git a/src/models/Event.ts b/src/models/Event.ts index a5903de..1fd65b0 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -12,6 +12,7 @@ export interface Event { end_time: string; tags: number[]; tag_id: number[]; + visible: boolean; } export async function getEvents(options: any = {}): Promise { @@ -42,7 +43,11 @@ export async function getEvent(id: number): Promise { export async function createEvent(data): Promise { try { - const resp = await axios.post(url, data); + const resp = await axios.post(url, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); return resp.data; } catch (err) { console.error(err); diff --git a/src/models/Feed.ts b/src/models/Feed.ts index 4032c01..6d7dac8 100644 --- a/src/models/Feed.ts +++ b/src/models/Feed.ts @@ -1,16 +1,21 @@ import axios from "axios"; +import { getAuthHeader } from "../auth"; const url = `${process.env.API_URL}/feed/`; -export interface Feed { +export interface Post { id: number; title: string; description: string; content: string; publish_time: string; + autohide: string; + tag_id: number[]; + tags: number[]; + visible: boolean; } -export async function getFeed(): Promise { +export async function getFeed(): Promise { try { const resp = await axios.get(url); return resp.data["results"]; @@ -19,3 +24,42 @@ export async function getFeed(): Promise { throw err; } } + +export async function getPost(id: number): Promise { + try { + const resp = await axios.get(`${url}${id}/`); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} + +export async function createPost(data): Promise { + try { + const resp = await axios.post(url, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} + +export async function updatePost(data): Promise { + try { + const putUrl = `${url}${data.id}/`; + const resp = await axios.put(putUrl, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} diff --git a/src/pages/AdminFeedPage/AdminFeedPage.tsx b/src/pages/AdminFeedPage/AdminFeedPage.tsx index 8f7f8f6..58ac79d 100644 --- a/src/pages/AdminFeedPage/AdminFeedPage.tsx +++ b/src/pages/AdminFeedPage/AdminFeedPage.tsx @@ -3,7 +3,7 @@ import Helmet from "react-helmet"; import { Link } from "react-router-dom"; import "./AdminFeedPage.scss"; import { StaticContext } from "../../server/StaticContext"; -import{Feed, getFeed} from "../../models/Feed"; +import { Post, getFeed} from "../../models/Feed"; import { getEvents } from "../../models/Event"; import { formatRelative } from "date-fns"; import { th } from "date-fns/esm/locale"; @@ -14,7 +14,7 @@ export interface AdminFeedPageProps { staticContext: StaticContext; } export interface AdminFeedPageState { - feed: Feed[]; + feed: Post[]; error?: string; } @@ -29,7 +29,7 @@ class AdminFeedPage extends React.Component { - const [date, rest] = value.split("T"); - const time = rest.slice(0, 5); - return ( -
- onChange(`${event.target.value}T${time}`)} - required={required} - value={date} /> - onChange(`${date}T${event.target.value}:00`)} - required={required} - value={time} /> -
- ); -}; +import DatetimeWidget from "../../components/DatetimeWidget"; const widgets = { - datetime: DateTimeWidget, + datetime: DatetimeWidget, }; export interface EventCreatePageProps { @@ -103,12 +85,14 @@ class EventCreatePage extends React.Component { - console.log("changed, data:"); - console.log(data); this.setState({ formData: data.formData, + statusMessage: null, }); } diff --git a/src/pages/FeedCreatePage/FeedCreatePage.scss b/src/pages/FeedCreatePage/FeedCreatePage.scss new file mode 100644 index 0000000..12cb7c6 --- /dev/null +++ b/src/pages/FeedCreatePage/FeedCreatePage.scss @@ -0,0 +1,65 @@ +@import "../../assets/scss/globals"; + +.post-create-page { + width: 100%; + + fieldset { + border: none; + padding: 0; + margin: 1rem 0; + } + + option { + padding: 4px 8px; + cursor: pointer; + } + + input[type="text"], + textarea { + padding: 0.5rem 0.5rem; + border: none; + overflow: visible; + box-sizing: border-box; + } + + input[type="text"], + textarea, + select, + .datetime-widget { + width: 20rem; + + @media screen and (max-width: 800px - 1px) { + width: 100%; + } + } + + legend { + font-weight: bold; + margin: 0.5rem 0; + } + + button { + background-color: $blue; + color: $white; + padding: 0.5rem 1rem; + border: none; + outline: none; + cursor: pointer; + } + + .checkbox { + label { + display: flex; + + input { + margin-right: 0.5rem; + } + } + } + + .datetime-widget { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + } +} diff --git a/src/pages/FeedCreatePage/FeedCreatePage.tsx b/src/pages/FeedCreatePage/FeedCreatePage.tsx new file mode 100644 index 0000000..00d9eca --- /dev/null +++ b/src/pages/FeedCreatePage/FeedCreatePage.tsx @@ -0,0 +1,228 @@ +import * as React from "react"; +import Helmet from "react-helmet"; +import "./FeedCreatePage.scss"; +import { isAuthenticated } from "../../auth"; +import Form from "react-jsonschema-form"; +import { Tag, getTags } from "../../models/Tag"; +import { createPost, getPost, updatePost } from "../../models/Feed"; +import DatetimeWidget from "../../components/DatetimeWidget"; + +const widgets = { + datetime: DatetimeWidget, +}; + +export interface FeedCreatePageProps { + history: { + push: (to: string) => void; + }; + match: { + params: { + id?: number; + }; + }; +} +export interface FeedCreatePageState { + tags: Tag[]; + error?: string; + statusMessage?: string; + formData: any; +} + +class FeedCreatePage extends React.Component { + constructor(props) { + super(props); + this.state = { + tags: [], + formData: {}, + }; + + this.fetchTags(); + + const id = props.match.params.id; + if (id !== undefined) { + this.fetchInitialFormData(id); + } + } + + fetchInitialFormData = async (id) => { + try { + const data = await getPost(id); + data.tags = data.tag_id; + this.setState({ + formData: data, + }); + } catch (err) { + this.setState({ + error: String(err), + }); + } + } + + fetchTags = async () => { + const getTagsPromise = getTags(); + try { + const tags = await getTagsPromise; + this.setState({ + tags, + }); + return getTagsPromise; + } catch (err) { + this.setState({ + error: String(err), + }); + } + } + + onSubmit = async (data) => { + console.log("submitted, data:"); + console.log(data); + + try { + const payload = data.formData; + payload.tag_id = payload.tags; + payload.autohide = payload.autohide || new Date(); + + if (payload.id === undefined) { + const resp = await createPost(payload); + resp.tags = resp.tag_id; + this.setState({ + formData: resp, + statusMessage: "Post created successfully", + }); + } else { + const resp = await updatePost(payload); + resp.tags = resp.tag_id; + this.setState({ + formData: resp, + statusMessage: "Post updated successfully.", + }); + } + } catch (err) { + this.setState({ + error: String(err), + }); + } + } + + onError = (data) => { + console.error("error, data:"); + console.log(data); + } + + onChange = (data) => { + this.setState({ + formData: data.formData, + statusMessage: null, + }); + } + + buildSchema = () => { + const { tags, error, formData } = this.state; + + const date = new Date(); + const currentDatetime = date.toISOString(); + + const schema = { + title: formData.id ? formData.title : "New Post", + type: "object", + required: ["title", "description", "content", "publish_time"], + properties: { + title: { + type: "string", + title: "Title", + default: "" + }, + tags: { + type: "array", + title: "Post tags", + items: { + type: "number", + enum: tags.map(t => t.id), + enumNames: tags.map(t => t.name), + }, + uniqueItems: true, + default: [], + }, + description: { + type: "string", + title: "Description", + default: "", + }, + content: { + type: "string", + title: "Content", + default: "", + }, + publish_time: { + type: "string", + title: "Publish time", + default: currentDatetime, + }, + autohide_enabled: { + type: "boolean", + title: "Autohide enabled", + default: false, + }, + autohide: { + type: "string", + title: "Autohide time", + default: "", + }, + visible: { + type: "boolean", + title: "Visible", + default: true, + } + } + }; + return schema; + } + + buildUISchema = () => { + const { formData } = this.state; + const { autohide_enabled } = formData; + const uiSchema = { + content: { + "ui:widget": "textarea", + }, + publish_time: { + "ui:widget": "datetime", + }, + autohide: { + "ui:widget": autohide_enabled ? "datetime" : "hidden", + }, + }; + return uiSchema; + } + + render() { + const { error, formData, statusMessage } = this.state; + const schema = this.buildSchema(); + const uiSchema = this.buildUISchema(); + + const title = formData.id + ? `Edit Post "${formData.title}"` + : "Create Post"; + + return ( +
+ + + +

{title}

+ { statusMessage &&
{ statusMessage }
} +
+ { error &&
{error}
} +
+ ); + } +} + +export default FeedCreatePage; diff --git a/src/pages/FeedCreatePage/index.ts b/src/pages/FeedCreatePage/index.ts new file mode 100644 index 0000000..162b402 --- /dev/null +++ b/src/pages/FeedCreatePage/index.ts @@ -0,0 +1,2 @@ +import FeedCreatePage from "./FeedCreatePage"; +export default FeedCreatePage; diff --git a/src/pages/FrontPage/FrontPage.tsx b/src/pages/FrontPage/FrontPage.tsx index f4558e2..2702694 100644 --- a/src/pages/FrontPage/FrontPage.tsx +++ b/src/pages/FrontPage/FrontPage.tsx @@ -4,7 +4,7 @@ import "./FrontPage.scss"; import appStore from "../../stores/AppStore"; import Card from "../../components/Card"; import { Event, getEvents } from "../../models/Event"; -import { Feed, getFeed } from "../../models/Feed"; +import { Post, getFeed } from "../../models/Feed"; import { StaticContext } from "../../server/StaticContext"; // @ts-ignore @@ -27,7 +27,7 @@ interface FrontPageProps { interface FrontPageState { events: Event[]; - feed: Feed[]; + feed: Post[]; } class FrontPage extends React.Component { @@ -42,7 +42,7 @@ class FrontPage extends React.Component { normally. See server/index.ts. */ if (staticContext.resolutions.getEvents) { const events = staticContext.resolutions.getEvents as Event[]; - const feed = staticContext.resolutions.getFeed as Feed[]; + const feed = staticContext.resolutions.getFeed as Post[]; this.state = { events, feed, diff --git a/src/routes.tsx b/src/routes.tsx index f37b76e..a8bab55 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -16,6 +16,7 @@ import AdminLoginPage from "./pages/AdminLoginPage"; import { getTokenCookie } from "./auth"; import AdminLogoutPage from "./pages/AdminLogoutPage"; import EventCreatePage from "./pages/EventCreatePage"; +import FeedCreatePage from "./pages/FeedCreatePage"; import ContactsPage from "./pages/ContactsPage"; const renderPage = (Page) => (props): JSX.Element => { @@ -43,6 +44,8 @@ const adminLoginRoutes = [ const adminRoutes = [ { path: "/admin/events", page: AdminEventPage }, { path: "/admin/feed", page: AdminFeedPage }, + { path: "/admin/feed/create", page: FeedCreatePage }, + { path: "/admin/feed/:id", page: FeedCreatePage }, { path: "/admin/events/create", page: EventCreatePage}, { path: "/admin/events/:id", page: EventCreatePage}, { path: "/admin/logout", page: AdminLogoutPage },