Implement Feed admin form

This commit is contained in:
Jan Tuomi
2019-03-13 13:52:02 +02:00
parent e912f89e5f
commit 82ce3fdf03
11 changed files with 404 additions and 31 deletions
@@ -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<DatetimeWidgetProps, DatetimeWidgetState> {
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 (
<div className="datetime-widget">
<input
type="date"
onChange={(event) => onChange(`${event.target.value}T${time}`)}
required={required}
disabled={disabled}
value={date} />
<input
type="time"
onChange={(event) => onChange(`${date}T${event.target.value}:00`)}
required={required}
disabled={disabled}
value={time} />
</div>
);
}
}
export default DatetimeWidget;
+2
View File
@@ -0,0 +1,2 @@
import DatetimeWidget from "./DatetimeWidget";
export default DatetimeWidget;
+6 -1
View File
@@ -12,6 +12,7 @@ export interface Event {
end_time: string;
tags: number[];
tag_id: number[];
visible: boolean;
}
export async function getEvents(options: any = {}): Promise<Event[]> {
@@ -42,7 +43,11 @@ export async function getEvent(id: number): Promise<Event> {
export async function createEvent(data): Promise<Event> {
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);
+46 -2
View File
@@ -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<Feed[]> {
export async function getFeed(): Promise<Post[]> {
try {
const resp = await axios.get(url);
return resp.data["results"];
@@ -19,3 +24,42 @@ export async function getFeed(): Promise<Feed[]> {
throw err;
}
}
export async function getPost(id: number): Promise<Post> {
try {
const resp = await axios.get(`${url}${id}/`);
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
export async function createPost(data): Promise<Post> {
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<Post> {
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;
}
}
+3 -3
View File
@@ -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<AdminFeedPageProps, AdminFeedPageSta
we have to store all promises in it. Otherwise, operate
normally. See server/index.ts. */
if (staticContext.resolutions.getFeed) {
const feed = staticContext.resolutions.getFeed as Feed[];
const feed = staticContext.resolutions.getFeed as Post[];
this.state = {
feed,
};
+5 -22
View File
@@ -5,28 +5,10 @@ import { isAuthenticated } from "../../auth";
import Form from "react-jsonschema-form";
import { Tag, getTags } from "../../models/Tag";
import { createEvent, getEvent, updateEvent } from "../../models/Event";
const DateTimeWidget = ({ value, onChange, required }) => {
const [date, rest] = value.split("T");
const time = rest.slice(0, 5);
return (
<div className="datetime-widget">
<input
type="date"
onChange={(event) => onChange(`${event.target.value}T${time}`)}
required={required}
value={date} />
<input
type="time"
onChange={(event) => onChange(`${date}T${event.target.value}:00`)}
required={required}
value={time} />
</div>
);
};
import DatetimeWidget from "../../components/DatetimeWidget";
const widgets = {
datetime: DateTimeWidget,
datetime: DatetimeWidget,
};
export interface EventCreatePageProps {
@@ -103,12 +85,14 @@ class EventCreatePage extends React.Component<EventCreatePageProps, EventCreateP
payload.tag_id = payload.tags;
if (payload.id === undefined) {
const resp = await createEvent(payload);
resp.tags = resp.tag_id;
this.setState({
formData: resp,
statusMessage: "Event created successfully",
});
} else {
const resp = await updateEvent(payload);
resp.tags = resp.tag_id;
this.setState({
formData: resp,
statusMessage: "Event updated successfully.",
@@ -127,10 +111,9 @@ class EventCreatePage extends React.Component<EventCreatePageProps, EventCreateP
}
onChange = (data) => {
console.log("changed, data:");
console.log(data);
this.setState({
formData: data.formData,
statusMessage: null,
});
}
@@ -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;
}
}
+228
View File
@@ -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<FeedCreatePageProps, FeedCreatePageState> {
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 (
<div className="post-create-page">
<Helmet>
<link rel="canonical" href="https://sik.ayy.fi/admin/feed/create" />
</Helmet>
<h1>{title}</h1>
{ statusMessage && <div className="success">{ statusMessage }</div>}
<Form schema={schema}
uiSchema={uiSchema}
formData={formData}
idPrefix="rjsf"
widgets={widgets}
onChange={this.onChange}
onSubmit={this.onSubmit}
onError={this.onError} />
{ error && <div className="error">{error}</div> }
</div>
);
}
}
export default FeedCreatePage;
+2
View File
@@ -0,0 +1,2 @@
import FeedCreatePage from "./FeedCreatePage";
export default FeedCreatePage;
+3 -3
View File
@@ -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<FrontPageProps, FrontPageState> {
@@ -42,7 +42,7 @@ class FrontPage extends React.Component<FrontPageProps, FrontPageState> {
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,
+3
View File
@@ -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 },