From f0ca49f71feb551775399cb010c37f3e89dddd44 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 13 Mar 2019 15:53:46 +0200 Subject: [PATCH] Add signup form stuff to admin --- package-lock.json | 59 +++--- package.json | 3 +- src/components/AdminSidebar/AdminSidebar.tsx | 1 + .../SignupQuestionsWidget.tsx | 63 ++++++ src/components/SignupQuestionsWidget/index.ts | 2 + src/models/SignupForm.ts | 63 ++++++ .../AdminSignupPage/AdminSignupPage.scss | 19 ++ src/pages/AdminSignupPage/AdminSignupPage.tsx | 119 +++++++++++ src/pages/AdminSignupPage/index.ts | 2 + .../SignupCreatePage/SignupCreatePage.scss | 65 ++++++ .../SignupCreatePage/SignupCreatePage.tsx | 190 ++++++++++++++++++ src/pages/SignupCreatePage/index.ts | 2 + src/routes.tsx | 7 +- 13 files changed, 561 insertions(+), 34 deletions(-) create mode 100644 src/components/SignupQuestionsWidget/SignupQuestionsWidget.tsx create mode 100644 src/components/SignupQuestionsWidget/index.ts create mode 100644 src/models/SignupForm.ts create mode 100644 src/pages/AdminSignupPage/AdminSignupPage.scss create mode 100644 src/pages/AdminSignupPage/AdminSignupPage.tsx create mode 100644 src/pages/AdminSignupPage/index.ts create mode 100644 src/pages/SignupCreatePage/SignupCreatePage.scss create mode 100644 src/pages/SignupCreatePage/SignupCreatePage.tsx create mode 100644 src/pages/SignupCreatePage/index.ts diff --git a/package-lock.json b/package-lock.json index 8dbfd7d..a6cf07f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6397,8 +6397,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6419,14 +6418,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6441,20 +6438,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6571,8 +6565,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6584,7 +6577,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6599,7 +6591,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6607,14 +6598,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6633,7 +6622,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6714,8 +6702,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6727,7 +6714,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6813,8 +6799,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6850,7 +6835,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6870,7 +6854,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6914,14 +6897,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -7123,7 +7104,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -11383,7 +11363,7 @@ }, "p-is-promise": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", "dev": true }, @@ -14717,6 +14697,21 @@ "jsonify": "~0.0.0" } }, + "shortid": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", + "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", + "requires": { + "nanoid": "^2.0.0" + }, + "dependencies": { + "nanoid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz", + "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==" + } + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index 3b8a138..c55e82f 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,8 @@ "react-helmet": "^5.2.0", "react-jsonschema-form": "^1.2.0", "react-router": "^4.3.1", - "react-router-dom": "^4.3.1" + "react-router-dom": "^4.3.1", + "shortid": "^2.2.14" }, "postcss": {} } diff --git a/src/components/AdminSidebar/AdminSidebar.tsx b/src/components/AdminSidebar/AdminSidebar.tsx index b7a6b25..0386314 100644 --- a/src/components/AdminSidebar/AdminSidebar.tsx +++ b/src/components/AdminSidebar/AdminSidebar.tsx @@ -15,6 +15,7 @@ class AdminSidebar extends React.Component Home Events Feed + Signup forms Logout ); diff --git a/src/components/SignupQuestionsWidget/SignupQuestionsWidget.tsx b/src/components/SignupQuestionsWidget/SignupQuestionsWidget.tsx new file mode 100644 index 0000000..3fb604b --- /dev/null +++ b/src/components/SignupQuestionsWidget/SignupQuestionsWidget.tsx @@ -0,0 +1,63 @@ +import * as React from "react"; +import * as shortid from "shortid"; + +export interface SignupQuestionsWidgetProps { + value: string; + onChange: (value: string) => void; + required: boolean; + disabled: boolean; +} +export interface SignupQuestionsWidgetState {} + +interface Question { + id: string; + name: string; + options: string[]; +} + +class SignupQuestionsWidget extends React.Component { + onValueChange = (questions: Question[]) => { + const { onChange } = this.props; + const newValue = JSON.stringify(questions); + onChange(newValue); + } + + handleNewRowClick = (questions) => () => { + const newQuestions = questions.concat([{ + id: shortid.generate(), + name: `Question #${questions.length + 1}`, + options: [], + }]); + + this.onValueChange(newQuestions); + } + + handleNameInputChange = (questions: Question[], index: number) => (event) => { + const val = event.target.value; + questions[index].name = val; + this.onValueChange(questions); + } + + renderQuestions = (questions: Question[]) => { + return questions.map((q, index) => ( +
+ +
+ )); + } + + render() { + const { value } = this.props; + + const questions = JSON.parse(value) as Question[]; + + return ( +
+ + {this.renderQuestions(questions)} +
+ ); + } +} + +export default SignupQuestionsWidget; diff --git a/src/components/SignupQuestionsWidget/index.ts b/src/components/SignupQuestionsWidget/index.ts new file mode 100644 index 0000000..5905d4e --- /dev/null +++ b/src/components/SignupQuestionsWidget/index.ts @@ -0,0 +1,2 @@ +import SignupQuestionsWidget from "./SignupQuestionsWidget"; +export default SignupQuestionsWidget; diff --git a/src/models/SignupForm.ts b/src/models/SignupForm.ts new file mode 100644 index 0000000..f9effa9 --- /dev/null +++ b/src/models/SignupForm.ts @@ -0,0 +1,63 @@ +import axios from "axios"; +import { getAuthHeader } from "../auth"; +import * as qs from "query-string"; +const url = `${process.env.API_URL}/signupForm/`; + +export interface SignupForm { + id: number; + title: string; + start_time: string; + end_time: string; + questions: any[]; + visible: boolean; +} + +export async function getForms(): Promise { + try { + const resp = await axios.get(url); + const results = resp.data["results"]; + return results; + } catch (err) { + console.error(err); + throw err; + } +} + +export async function getForm(id: number): Promise { + try { + const resp = await axios.get(`${url}${id}/`); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} + +export async function createForm(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 updateForm(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/AdminSignupPage/AdminSignupPage.scss b/src/pages/AdminSignupPage/AdminSignupPage.scss new file mode 100644 index 0000000..aea0a80 --- /dev/null +++ b/src/pages/AdminSignupPage/AdminSignupPage.scss @@ -0,0 +1,19 @@ +@import "../../assets/scss/globals"; + +.admin-signup-page { + table { + border-collapse: collapse; + } + + table, + th, + td { + border: 1px solid $white; + padding: 0.5rem; + + a { + color: $orange1; + text-decoration: underline; + } + } +} diff --git a/src/pages/AdminSignupPage/AdminSignupPage.tsx b/src/pages/AdminSignupPage/AdminSignupPage.tsx new file mode 100644 index 0000000..35f1c28 --- /dev/null +++ b/src/pages/AdminSignupPage/AdminSignupPage.tsx @@ -0,0 +1,119 @@ +import * as React from "react"; +import Helmet from "react-helmet"; +import { Link } from "react-router-dom"; +import { formatRelative } from "date-fns"; + +import "./AdminSignupPage.scss"; +import { SignupForm, getForms } from "../../models/SignupForm"; +import { StaticContext } from "../../server/StaticContext"; +// @ts-ignore +import * as AddIcon from "../../assets/img/add-icon.png"; + + +export interface AdminSignupPageProps { + staticContext: StaticContext; +} +export interface AdminSignupPageState { + signupForms: SignupForm[]; + error?: string; +} + +class AdminSignupPage extends React.Component { + constructor(props: AdminSignupPageProps) { + super(props); + const { staticContext } = props; + + if (staticContext) { + /* The static context is an object that manages promises when + rendering on the server. If staticContext exists, that means + we have to store all promises in it. Otherwise, operate + normally. See server/index.ts. */ + if (staticContext.resolutions.getSignupForms) { + const signupForms = staticContext.resolutions.getSignupForms as SignupForm[]; + this.state = { + signupForms, + }; + } else { + this.state = { + signupForms: [], + }; + const promiseSignupForms = this.fetchSignupForms(); + staticContext.promises.getSignupForms = promiseSignupForms; + } + } else { + this.state = { + signupForms: [], + }; + this.fetchSignupForms(); + } + } + + fetchSignupForms = async () => { + const getSignupFormsPromise = getForms(); + try { + const signupForms = await getSignupFormsPromise; + this.setState({ + signupForms, + }); + return getSignupFormsPromise; + } catch (err) { + this.setState({ + error: String(err), + }); + } + } + + renderAddLink = () => ( + + Create signup form + + ) + + renderData = () => { + const { signupForms, error } = this.state; + + if (error) { + return
{ error }
; + } + + if (!signupForms || signupForms.length === 0) { + return
No signup forms.
; + } + + return ( + + + + + + + + + + {signupForms.map(signupForm => ( + + + + + + ))} + +
TitleStart timeEnd time
{signupForm.title}{formatRelative(new Date(signupForm.start_time), new Date())}{formatRelative(new Date(signupForm.end_time), new Date())}
+ ); + } + + render() { + return ( +
+ + + +

Sign-up forms

+ { this.renderAddLink() } + { this.renderData() } +
+ ); + } +} + +export default AdminSignupPage; diff --git a/src/pages/AdminSignupPage/index.ts b/src/pages/AdminSignupPage/index.ts new file mode 100644 index 0000000..c356fc4 --- /dev/null +++ b/src/pages/AdminSignupPage/index.ts @@ -0,0 +1,2 @@ +import AdminSignupPage from "./AdminSignupPage"; +export default AdminSignupPage; diff --git a/src/pages/SignupCreatePage/SignupCreatePage.scss b/src/pages/SignupCreatePage/SignupCreatePage.scss new file mode 100644 index 0000000..cae0831 --- /dev/null +++ b/src/pages/SignupCreatePage/SignupCreatePage.scss @@ -0,0 +1,65 @@ +@import "../../assets/scss/globals"; + +.signup-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/SignupCreatePage/SignupCreatePage.tsx b/src/pages/SignupCreatePage/SignupCreatePage.tsx new file mode 100644 index 0000000..6d6f74c --- /dev/null +++ b/src/pages/SignupCreatePage/SignupCreatePage.tsx @@ -0,0 +1,190 @@ +import * as React from "react"; +import Helmet from "react-helmet"; +import "./SignupCreatePage.scss"; +import { isAuthenticated } from "../../auth"; +import Form from "react-jsonschema-form"; +import { Tag, getTags } from "../../models/Tag"; +import { createForm, getForm, updateForm, deserializeForm } from "../../models/SignupForm"; +import DatetimeWidget from "../../components/DatetimeWidget"; +import SignupQuestionsWidget from "../../components/SignupQuestionsWidget"; + +const widgets = { + datetime: DatetimeWidget, + signup: SignupQuestionsWidget, +}; + +export interface SignupCreatePageProps { + history: { + push: (to: string) => void; + }; + match: { + params: { + id?: number; + }; + }; +} +export interface SignupCreatePageState { + error?: string; + statusMessage?: string; + formData: any; +} + +class SignupCreatePage extends React.Component { + constructor(props) { + super(props); + this.state = { + formData: {}, + }; + + const id = props.match.params.id; + if (id !== undefined) { + this.fetchInitialFormData(id); + } + } + + fetchInitialFormData = async (id) => { + try { + const data = await getForm(id); + this.setState({ + formData: data, + }); + } catch (err) { + this.setState({ + error: String(err), + }); + } + } + + onSubmit = async (data) => { + console.log("submitted, data:"); + console.log(data); + + try { + const payload = data.formData; + // payload.questions = JSON.stringify(payload.questions); + + if (payload.id === undefined) { + const resp = await createForm(payload); + this.setState({ + formData: resp, + statusMessage: "Sign-up created successfully", + }); + } else { + const resp = await updateForm(payload); + this.setState({ + formData: resp, + statusMessage: "Sign-up 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 { error, formData } = this.state; + + const date = new Date(); + const currentDatetime = date.toISOString(); + const tomorrowDate = new Date(); + tomorrowDate.setDate(tomorrowDate.getDate() + 1); + const tomorrowDatetime = tomorrowDate.toISOString(); + + const schema = { + title: formData.id ? formData.title : "New Sign-up form", + type: "object", + required: ["title", "start_time", "end_time", "questions"], + properties: { + title: { + type: "string", + title: "Title", + default: "", + }, + visible: { + type: "boolean", + title: "Visible", + default: true, + }, + start_time: { + type: "string", + title: "Start time", + default: currentDatetime, + }, + end_time: { + type: "string", + title: "End time", + default: tomorrowDatetime, + }, + questions: { + type: "string", + title: "Questions", + default: "[]", + }, + } + }; + return schema; + } + + buildUISchema = () => { + const uiSchema = { + content: { + "ui:widget": "textarea", + }, + start_time: { + "ui:widget": "datetime", + }, + end_time: { + "ui:widget": "datetime", + }, + questions: { + "ui:widget": "signup", + }, + }; + return uiSchema; + } + + render() { + const { error, formData, statusMessage } = this.state; + const schema = this.buildSchema(); + const uiSchema = this.buildUISchema(); + + const title = formData.id + ? `Edit Sign-up Form "${formData.title}"` + : "Create Sign-up form"; + + return ( +
+ + + +

{title}

+ { statusMessage &&
{ statusMessage }
} +
+ { error &&
{error}
} +
+ ); + } +} + +export default SignupCreatePage; diff --git a/src/pages/SignupCreatePage/index.ts b/src/pages/SignupCreatePage/index.ts new file mode 100644 index 0000000..753e7a8 --- /dev/null +++ b/src/pages/SignupCreatePage/index.ts @@ -0,0 +1,2 @@ +import SignupCreatePage from "./SignupCreatePage"; +export default SignupCreatePage; diff --git a/src/routes.tsx b/src/routes.tsx index a8bab55..cbd2284 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -18,6 +18,8 @@ import AdminLogoutPage from "./pages/AdminLogoutPage"; import EventCreatePage from "./pages/EventCreatePage"; import FeedCreatePage from "./pages/FeedCreatePage"; import ContactsPage from "./pages/ContactsPage"; +import AdminSignupPage from "./pages/AdminSignupPage"; +import SignupCreatePage from "./pages/SignupCreatePage"; const renderPage = (Page) => (props): JSX.Element => { return ; @@ -42,12 +44,15 @@ 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", page: AdminEventPage }, { path: "/admin/events/create", page: EventCreatePage}, { path: "/admin/events/:id", page: EventCreatePage}, + { path: "/admin/signups", page: AdminSignupPage }, + { path: "/admin/signups/create", page: SignupCreatePage}, + { path: "/admin/signups/:id", page: SignupCreatePage}, { path: "/admin/logout", page: AdminLogoutPage }, { path: "/admin", page: AdminFrontPage }, ];