Add signup form stuff to admin

This commit is contained in:
Jan Tuomi
2019-03-13 15:53:46 +02:00
parent 82ce3fdf03
commit f0ca49f71f
13 changed files with 561 additions and 34 deletions
+27 -32
View File
@@ -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",
+2 -1
View File
@@ -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": {}
}
@@ -15,6 +15,7 @@ class AdminSidebar extends React.Component<AdminSidebarProps, AdminSidebarState>
<AdminSidebarLink to="/admin" path={path}>Home</AdminSidebarLink>
<AdminSidebarLink to="/admin/events" path={path}>Events</AdminSidebarLink>
<AdminSidebarLink to="/admin/feed" path={path}>Feed</AdminSidebarLink>
<AdminSidebarLink to="/admin/signups" path={path}>Signup forms</AdminSidebarLink>
<AdminSidebarLink id="admin-sidebar-logout" to="/admin/logout" path={path}>Logout</AdminSidebarLink>
</div>
);
@@ -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<SignupQuestionsWidgetProps, SignupQuestionsWidgetState> {
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) => (
<div key={q.id} className="signup-questions-widget-row">
<input type="text" value={ q.name } onChange={this.handleNameInputChange(questions, index)} />
</div>
));
}
render() {
const { value } = this.props;
const questions = JSON.parse(value) as Question[];
return (
<div className="signup-questions-widget">
<button type="button" onClick={this.handleNewRowClick(questions)}>New Row</button>
{this.renderQuestions(questions)}
</div>
);
}
}
export default SignupQuestionsWidget;
@@ -0,0 +1,2 @@
import SignupQuestionsWidget from "./SignupQuestionsWidget";
export default SignupQuestionsWidget;
+63
View File
@@ -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<SignupForm[]> {
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<SignupForm> {
try {
const resp = await axios.get(`${url}${id}/`);
return resp.data;
} catch (err) {
console.error(err);
throw err;
}
}
export async function createForm(data): Promise<SignupForm> {
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<SignupForm> {
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;
}
}
@@ -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;
}
}
}
@@ -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<AdminSignupPageProps, AdminSignupPageState> {
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 = () => (
<Link className="add-link" to="/admin/signups/create">
<img src={AddIcon} /> Create signup form
</Link>
)
renderData = () => {
const { signupForms, error } = this.state;
if (error) {
return <div className="error">{ error }</div>;
}
if (!signupForms || signupForms.length === 0) {
return <div>No signup forms.</div>;
}
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Start time</th>
<th>End time</th>
</tr>
</thead>
<tbody>
{signupForms.map(signupForm => (
<tr key={signupForm.id}>
<td><Link to={`/admin/signups/${signupForm.id}`}>{signupForm.title}</Link></td>
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
</tr>
))}
</tbody>
</table>
);
}
render() {
return (
<div className="admin-signup-page">
<Helmet>
<link rel="canonical" href="https://sik.ayy.fi/admin/events" />
</Helmet>
<h1>Sign-up forms</h1>
{ this.renderAddLink() }
{ this.renderData() }
</div>
);
}
}
export default AdminSignupPage;
+2
View File
@@ -0,0 +1,2 @@
import AdminSignupPage from "./AdminSignupPage";
export default AdminSignupPage;
@@ -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;
}
}
@@ -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<SignupCreatePageProps, SignupCreatePageState> {
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 (
<div className="signup-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 SignupCreatePage;
+2
View File
@@ -0,0 +1,2 @@
import SignupCreatePage from "./SignupCreatePage";
export default SignupCreatePage;
+6 -1
View File
@@ -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 <CommonPage page={Page} {...props} />;
@@ -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 },
];