Merge branch 'bugfix/admin-fixes' into 'master'
Bugfix: Numerous admin fixes See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!16
This commit is contained in:
Generated
+1990
-587
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -55,8 +55,8 @@
|
||||
"@types/react-jsonschema-form": "1.7.3",
|
||||
"@types/react-router-dom": "5.1.5",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "2.6.1",
|
||||
"@typescript-eslint/parser": "2.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.2",
|
||||
"@typescript-eslint/parser": "^4.8.2",
|
||||
"babel-cli": "6.26.0",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-loader": "7.1.5",
|
||||
@@ -68,14 +68,14 @@
|
||||
"css-loader": "2.1.1",
|
||||
"dotenv": "6.2.0",
|
||||
"dotenv-webpack": "1.7.0",
|
||||
"eslint": "6.6.0",
|
||||
"eslint-config-standard": "14.1.0",
|
||||
"eslint-plugin-import": "2.18.2",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-node": "10.0.0",
|
||||
"eslint-plugin-promise": "4.2.1",
|
||||
"eslint-plugin-react": "7.16.0",
|
||||
"eslint-plugin-standard": "4.0.1",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"express": "4.17.0",
|
||||
"favicons-webpack-plugin": "1.0.2",
|
||||
"file-loader": "4.2.0",
|
||||
@@ -94,7 +94,7 @@
|
||||
"react-addons-test-utils": "15.6.2",
|
||||
"react-dom": "16.8.6",
|
||||
"react-hot-loader": "4.8.8",
|
||||
"sass": "^1.26.8",
|
||||
"sass": "1.29.0",
|
||||
"sass-loader": "7.1.0",
|
||||
"serve": "11.3.2",
|
||||
"style-loader": "0.21.0",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { ComponentProps } from "react";
|
||||
import styled from "styled-components";
|
||||
import { colors }from "@theme/colors";
|
||||
import Anchor from "@components/Anchor";
|
||||
import AddIcon from "@assets/img/add-icon.png";
|
||||
|
||||
const Link = styled(Anchor)`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: ${colors.orange2};
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 8px;
|
||||
margin-top: -2px;
|
||||
width: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
type AddLinkProps = ComponentProps<typeof Anchor> & {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const AddLink: React.FC<AddLinkProps> = ({ text, ...props }) => (
|
||||
<Link {...props}>
|
||||
<img src={AddIcon} />
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default AddLink;
|
||||
@@ -4,8 +4,6 @@ import { Link } from "react-router-dom";
|
||||
import TitleImage from "@assets/img/SIK_RGB_W_side.png";
|
||||
|
||||
const Header = styled.header`
|
||||
margin: 0.5rem;
|
||||
|
||||
a {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
@@ -13,6 +11,7 @@ const Header = styled.header`
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
margin: 0.5rem;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
@@ -21,7 +20,7 @@ const Header = styled.header`
|
||||
`;
|
||||
|
||||
const AdminHeader: React.FC = () => (
|
||||
<Header className="header admin-header">
|
||||
<Header className="header">
|
||||
<Link to="/">
|
||||
<img src={TitleImage} />
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import AdminSidebarLink from "./AdminSidebarLink";
|
||||
import Anchor from "@components/Anchor";
|
||||
import { colors } from "@theme/colors";
|
||||
|
||||
interface AdminSidebarProps {
|
||||
path: string;
|
||||
@@ -9,24 +10,47 @@ interface AdminSidebarProps {
|
||||
const SideBar = styled.nav`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-self: stretch;
|
||||
margin-right: 1rem;
|
||||
background-color: ${colors.blue1};
|
||||
|
||||
@media screen and (max-width: 800px - 1px) {
|
||||
@media screen and (max-width: 800px) {
|
||||
margin-right: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Anchor)<{path: string}>`
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
color: ${colors.white};
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
${p => p.path === p.to && `
|
||||
border-left: 4px solid ${colors.white};
|
||||
`}
|
||||
|
||||
&:hover {
|
||||
border-left: 4px solid ${colors.white};
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AdminSidebar: React.FC<AdminSidebarProps> = ({ path }) => (
|
||||
<SideBar>
|
||||
<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 to="/admin/jobads" path={path}>Job advertisements</AdminSidebarLink>
|
||||
<AdminSidebarLink to="https://static.sika.sik.party/admin" path={path}>Files</AdminSidebarLink>
|
||||
<AdminSidebarLink id="admin-sidebar-logout" to="/admin/logout" path={path}>Logout</AdminSidebarLink>
|
||||
<StyledLink to="/admin" path={path}>Home ›</StyledLink>
|
||||
<StyledLink to="/admin/events" path={path}>Events ›</StyledLink>
|
||||
<StyledLink to="/admin/feed" path={path}>Feed ›</StyledLink>
|
||||
<StyledLink to="/admin/signups" path={path}>Signup forms ›</StyledLink>
|
||||
<StyledLink to="/admin/jobads" path={path}>Job advertisements ›</StyledLink>
|
||||
<StyledLink to="https://static.sika.sik.party/admin" path={path}>Files ›</StyledLink>
|
||||
<StyledLink id="admin-sidebar-logout" to="/admin/logout" path={path}>Logout ›</StyledLink>
|
||||
</SideBar>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
|
||||
.admin-sidebar-link {
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
background-color: color(blue1);
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
border-left: 4px solid color(blue1);
|
||||
white-space: nowrap;
|
||||
|
||||
@media screen and (max-width: 800px - 1px) {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-left: 4px solid color(white1);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: " ›";
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from "react";
|
||||
import Anchor from "../Anchor";
|
||||
import "./AdminSidebarLink.scss";
|
||||
|
||||
export interface AdminSidebarLinkProps {
|
||||
to: string;
|
||||
path: string;
|
||||
id?: string;
|
||||
}
|
||||
export interface AdminSidebarLinkState { }
|
||||
|
||||
class AdminSidebarLink extends React.Component<AdminSidebarLinkProps, AdminSidebarLinkState> {
|
||||
render() {
|
||||
const { to, path, children, id } = this.props;
|
||||
const activeClass = to === path ? "active" : "";
|
||||
|
||||
return (
|
||||
<Anchor id={id} to={to} className={`admin-sidebar-link ${activeClass}`}>
|
||||
{children}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminSidebarLink;
|
||||
@@ -1,2 +0,0 @@
|
||||
import AdminSidebarLink from "./AdminSidebarLink";
|
||||
export default AdminSidebarLink;
|
||||
+10
-14
@@ -1,20 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
export interface JsonLDProps {
|
||||
data: object;
|
||||
interface JsonLDProps {
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class JsonLD extends React.Component<JsonLDProps, undefined> {
|
||||
render() {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(this.props.data),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const JsonLD: React.FC<JsonLDProps> = ({ data }) => (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(data),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default JsonLD;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import "./DatetimeWidget.scss";
|
||||
|
||||
export interface DatetimeWidgetProps {
|
||||
interface DatetimeWidgetProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onFocus: () => void;
|
||||
@@ -9,42 +9,37 @@ export interface DatetimeWidgetProps {
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
export interface DatetimeWidgetState { }
|
||||
|
||||
class DatetimeWidget extends React.Component<DatetimeWidgetProps, DatetimeWidgetState> {
|
||||
render() {
|
||||
const { value, onChange, onFocus, onBlur, required, disabled } = this.props;
|
||||
|
||||
let date;
|
||||
let time;
|
||||
if (value && value.length !== 0) {
|
||||
let rest;
|
||||
[date, rest] = value.split("T");
|
||||
time = rest.slice(0, 5);
|
||||
}
|
||||
|
||||
const commonProps = {
|
||||
onFocus,
|
||||
onBlur,
|
||||
required,
|
||||
disabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="datetime-widget">
|
||||
<input
|
||||
type="date"
|
||||
onChange={(event) => onChange(`${event.target.value}T${time}`)}
|
||||
value={date}
|
||||
{...commonProps} />
|
||||
<input
|
||||
type="time"
|
||||
onChange={(event) => onChange(`${date}T${event.target.value}:00`)}
|
||||
value={time}
|
||||
{...commonProps} />
|
||||
</div>
|
||||
);
|
||||
const DatetimeWidget: React.FC<DatetimeWidgetProps> = ({ value, onChange, onFocus, onBlur, required, disabled }) => {
|
||||
let date;
|
||||
let time;
|
||||
if (value && value.length !== 0) {
|
||||
let rest;
|
||||
[date, rest] = value.split("T");
|
||||
time = rest.slice(0, 5);
|
||||
}
|
||||
|
||||
const commonProps = {
|
||||
onFocus,
|
||||
onBlur,
|
||||
required,
|
||||
disabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="datetime-widget">
|
||||
<input
|
||||
type="date"
|
||||
onChange={(event) => onChange(`${event.target.value}T${time}`)}
|
||||
value={date}
|
||||
{...commonProps} />
|
||||
<input
|
||||
type="time"
|
||||
onChange={(event) => onChange(`${date}T${event.target.value}:00`)}
|
||||
value={time}
|
||||
{...commonProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatetimeWidget;
|
||||
|
||||
@@ -3,10 +3,9 @@ import "./SectionDividerWidget.scss";
|
||||
import Icon from "../../Icon";
|
||||
import { IconType } from "../../Icon/Icon";
|
||||
|
||||
export interface SectionDividerWidgetProps {
|
||||
interface SectionDividerWidgetProps {
|
||||
label: string;
|
||||
}
|
||||
export interface SectionDividerWidgetState { }
|
||||
|
||||
const getIconByLabel = (label: string) => {
|
||||
if (label === "Finnish") {
|
||||
@@ -19,16 +18,10 @@ const getIconByLabel = (label: string) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
class SectionDividerWidget extends React.Component<SectionDividerWidgetProps, SectionDividerWidgetState> {
|
||||
render() {
|
||||
const { label } = this.props;
|
||||
|
||||
return (
|
||||
<h3 className="section-divider-widget">
|
||||
{label} {getIconByLabel(label)}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
}
|
||||
const SectionDividerWidget: React.FC<SectionDividerWidgetProps> = ({ label }) => (
|
||||
<h3 className="section-divider-widget">
|
||||
{label} {getIconByLabel(label)}
|
||||
</h3>
|
||||
);
|
||||
|
||||
export default SectionDividerWidget;
|
||||
|
||||
@@ -6,23 +6,21 @@ import AddIcon from "@assets/img/add-icon.png";
|
||||
import "./SignupQuestionsWidget.scss";
|
||||
import QuestionList from "./QuestionList";
|
||||
|
||||
export interface SignupQuestionsWidgetProps {
|
||||
interface SignupQuestionsWidgetProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onFocus: () => void;
|
||||
required: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
export interface SignupQuestionsWidgetState { }
|
||||
|
||||
class SignupQuestionsWidget extends React.Component<SignupQuestionsWidgetProps, SignupQuestionsWidgetState> {
|
||||
onValueChange = (questions: Question[]) => {
|
||||
const { onChange } = this.props;
|
||||
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
|
||||
const onValueChange = (questions: Question[]) => {
|
||||
const newValue = JSON.stringify(questions);
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
handleNewRowClick = (questions) => () => {
|
||||
const handleNewRowClick = (questions) => () => {
|
||||
const newRow: Question = {
|
||||
id: shortid.generate(),
|
||||
name: `Question #${questions.length + 1}`,
|
||||
@@ -31,47 +29,42 @@ class SignupQuestionsWidget extends React.Component<SignupQuestionsWidgetProps,
|
||||
};
|
||||
const newQuestions: Question[] = questions.concat([newRow]);
|
||||
|
||||
this.onValueChange(newQuestions);
|
||||
onValueChange(newQuestions);
|
||||
}
|
||||
|
||||
handleDragEnd = (questions: Question[]) => (result) => {
|
||||
const handleDragEnd = (questions: Question[]) => (result) => {
|
||||
const srcIndex = result.source.index;
|
||||
const dstIndex = result.destination.index;
|
||||
const srcCopy = { ...questions[srcIndex] };
|
||||
questions.splice(srcIndex, 1);
|
||||
questions.splice(dstIndex, 0, srcCopy);
|
||||
|
||||
this.onValueChange(questions);
|
||||
onValueChange(questions);
|
||||
}
|
||||
const questions = JSON.parse(value) as Question[];
|
||||
|
||||
render() {
|
||||
const { value, onFocus } = this.props;
|
||||
|
||||
const questions = JSON.parse(value) as Question[];
|
||||
|
||||
return (
|
||||
<div className="signup-questions-widget">
|
||||
<DragDropContext
|
||||
onDragEnd={this.handleDragEnd(questions)}
|
||||
onDragStart={onFocus}
|
||||
>
|
||||
<Droppable droppableId="questions">
|
||||
{(provided) => (
|
||||
<QuestionList
|
||||
{...provided.droppableProps}
|
||||
innerRef={provided.innerRef}
|
||||
questions={questions}
|
||||
onChange={this.onValueChange}
|
||||
placeholder={provided.placeholder} />
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<button type="button" className="add-link" onClick={this.handleNewRowClick(questions)}>
|
||||
<img src={AddIcon} /> New Question
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="signup-questions-widget">
|
||||
<DragDropContext
|
||||
onDragEnd={handleDragEnd(questions)}
|
||||
onDragStart={onFocus}
|
||||
>
|
||||
<Droppable droppableId="questions">
|
||||
{(provided) => (
|
||||
<QuestionList
|
||||
{...provided.droppableProps}
|
||||
innerRef={provided.innerRef}
|
||||
questions={questions}
|
||||
onChange={onValueChange}
|
||||
placeholder={provided.placeholder} />
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<button type="button" className="add-link" onClick={handleNewRowClick(questions)}>
|
||||
<img src={AddIcon} /> New Question
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignupQuestionsWidget;
|
||||
|
||||
@@ -23,6 +23,7 @@ declare const module: { hot: any };
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept("./routes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const NewRoutes = require("./routes").default;
|
||||
|
||||
render(
|
||||
|
||||
+7
-1
@@ -23,7 +23,13 @@ export interface Event {
|
||||
signupForm: SignupForm[];
|
||||
}
|
||||
|
||||
export async function getEvents(options: any = {}): Promise<Event[]> {
|
||||
interface Options {
|
||||
onlyNonPast?: boolean;
|
||||
limit?: number;
|
||||
auth?: boolean;
|
||||
}
|
||||
|
||||
export async function getEvents(options: Options = {}): Promise<Event[]> {
|
||||
const { onlyNonPast, limit, auth } = options;
|
||||
try {
|
||||
const params = {
|
||||
|
||||
+13
-4
@@ -1,10 +1,14 @@
|
||||
import axios from "axios";
|
||||
import { getAuthHeader } from "@utils/auth";
|
||||
import { Tag } from "./Tag";
|
||||
|
||||
const url = `${process.env.API_URL}/feed/`;
|
||||
|
||||
export interface Post {
|
||||
id: number;
|
||||
tags: Tag[];
|
||||
visible: boolean;
|
||||
image: string;
|
||||
title_fi: string;
|
||||
title_en: string;
|
||||
description_fi: string;
|
||||
@@ -13,13 +17,18 @@ export interface Post {
|
||||
content_en: string;
|
||||
publish_time: string;
|
||||
autohide: string;
|
||||
tags: number[];
|
||||
visible: boolean;
|
||||
autohide_enabled: boolean;
|
||||
}
|
||||
|
||||
export async function getFeed(): Promise<Post[]> {
|
||||
interface Options {
|
||||
auth?: boolean;
|
||||
}
|
||||
|
||||
export async function getFeed(options: Options = {}): Promise<Post[]> {
|
||||
const { auth } = options;
|
||||
const headers = auth ? { "Authorization": getAuthHeader() } : null;
|
||||
try {
|
||||
const resp = await axios.get(url);
|
||||
const resp = await axios.get(url, { headers });
|
||||
return resp.data["results"];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
|
||||
.admin-container {
|
||||
display: flex;
|
||||
background-color: color(dark-blue);
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
|
||||
@media screen and (max-width: 800px - 1px) {
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
> div[class$='page'] {
|
||||
padding: 0 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rjsf {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
padding: 0.3rem 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: color(orange2);
|
||||
padding: 0.5rem 1rem;
|
||||
color: color(white1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid color(orange2);
|
||||
padding: 8px 16px;
|
||||
color: color(orange2);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.success {
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid color(green1);
|
||||
padding: 8px 16px;
|
||||
color: color(green1);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.add-link {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:hover {
|
||||
color: color(orange2);
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 8px;
|
||||
margin-top: -2px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,37 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { colors }from "@theme/colors";
|
||||
import AdminHeader from "@components/AdminHeader";
|
||||
import AdminSidebar from "@components/AdminSidebar";
|
||||
import { isAuthenticated } from "@utils/auth";
|
||||
import "./AdminCommonPage.scss";
|
||||
|
||||
const Main = styled.main`
|
||||
display: flex;
|
||||
color: ${colors.white};
|
||||
background-color: ${colors.darkBlue};
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
flex-flow: column nowrap;
|
||||
|
||||
& > nav {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
padding: 0 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface AdminCommonPageProps {
|
||||
page: any;
|
||||
@@ -56,10 +84,10 @@ class AdminCommonPage extends React.Component<AdminCommonPageProps, AdminCommonP
|
||||
return (
|
||||
<>
|
||||
<AdminHeader />
|
||||
<div className="admin-container">
|
||||
<Main>
|
||||
<AdminSidebar path={path} />
|
||||
<Page {...this.props} />
|
||||
</div>
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Form, { ISubmitEvent, IChangeEvent, ErrorSchema } from "react-jsonschema-form";
|
||||
import { colors }from "@theme/colors";
|
||||
import { Event } from "@models/Event";
|
||||
import { Post } from "@models/Feed";
|
||||
import { SignupForm } from "@models/SignupForm";
|
||||
import { JobAd } from "@models/JobAd";
|
||||
|
||||
const Common = styled.div`
|
||||
width: 100%;
|
||||
|
||||
.rjsf {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
option {
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
padding: 0.3rem 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
textarea {
|
||||
padding: 0.5rem 0.5rem;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: ${colors.orange2};
|
||||
padding: 0.5rem 1rem;
|
||||
color: ${colors.white};
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: ${colors.blue1};
|
||||
color: ${colors.white};
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SuccessMsg = styled.p`
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid ${colors.green1};
|
||||
padding: 8px 16px;
|
||||
color: ${colors.green1};
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const ErrorMsg = styled.p`
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid ${colors.orange2};
|
||||
padding: 8px 16px;
|
||||
color: ${colors.orange2};
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
type FormTypes = Event | SignupForm | Post | JobAd;
|
||||
|
||||
type AdminCreateCommonProps = {
|
||||
title: string;
|
||||
formData?: FormTypes;
|
||||
schema: {
|
||||
[name: string]: any;
|
||||
};
|
||||
UISchema: {
|
||||
[name: string]: any;
|
||||
};
|
||||
onChange: (e: IChangeEvent<FormTypes>, es?: ErrorSchema) => any;
|
||||
onFocus: (id: string, value: string | number | boolean) => void;
|
||||
onSubmit: (e: ISubmitEvent<FormTypes>) => any;
|
||||
statusMessage: string;
|
||||
error: string;
|
||||
widgets: {
|
||||
[name: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
const AdminCreateCommon: React.FC<AdminCreateCommonProps> = ({
|
||||
title,
|
||||
formData,
|
||||
schema,
|
||||
UISchema,
|
||||
onChange,
|
||||
onFocus,
|
||||
onSubmit,
|
||||
statusMessage,
|
||||
error,
|
||||
widgets
|
||||
}) => {
|
||||
|
||||
const onError = (data: any) => {
|
||||
console.error("error, data:");
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<Common>
|
||||
<h1>{title}</h1>
|
||||
{statusMessage && (
|
||||
<SuccessMsg>{statusMessage}</SuccessMsg>
|
||||
)}
|
||||
<Form
|
||||
schema={schema}
|
||||
uiSchema={UISchema}
|
||||
formData={formData}
|
||||
idPrefix="rjsf"
|
||||
widgets={widgets}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
onError={onError}
|
||||
onFocus={onFocus} />
|
||||
|
||||
{error && (
|
||||
<ErrorMsg>{error}</ErrorMsg>
|
||||
)}
|
||||
</Common>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminCreateCommon;
|
||||
@@ -1,20 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
|
||||
.admin-event-page {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid color(white1);
|
||||
padding: 0.5rem;
|
||||
|
||||
a {
|
||||
color: color(orange1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +1,57 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import Anchor from "@components/Anchor";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
import "./AdminEventPage.scss";
|
||||
import AdminListCommon from "./AdminListCommon";
|
||||
import Anchor from "@components/Anchor";
|
||||
import AddLink from "@components/AddLink";
|
||||
import { Event, getEvents } from "@models/Event";
|
||||
import { StaticContext } from "@server/StaticContext";
|
||||
import AddIcon from "@assets/img/add-icon.png";
|
||||
|
||||
const URL = "/admin/events"
|
||||
|
||||
export interface AdminEventPageProps {
|
||||
staticContext: StaticContext;
|
||||
}
|
||||
export interface AdminEventPageState {
|
||||
events: Event[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class AdminEventPage extends React.Component<AdminEventPageProps, AdminEventPageState> {
|
||||
constructor(props: AdminEventPageProps) {
|
||||
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.getEvents) {
|
||||
const events = staticContext.resolutions.getEvents as Event[];
|
||||
this.state = {
|
||||
events,
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
events: [],
|
||||
};
|
||||
const promiseEvents = this.fetchEvents();
|
||||
staticContext.promises.getEvents = promiseEvents;
|
||||
}
|
||||
} else {
|
||||
this.state = {
|
||||
events: [],
|
||||
};
|
||||
this.fetchEvents();
|
||||
}
|
||||
const renderData = (events: Event[]) => {
|
||||
if (!events || events.length === 0) {
|
||||
return <div>No events.</div>;
|
||||
}
|
||||
|
||||
fetchEvents = async () => {
|
||||
const getEventsPromise = getEvents({ auth: true });
|
||||
try {
|
||||
const events = await getEventsPromise;
|
||||
this.setState({
|
||||
events,
|
||||
});
|
||||
return getEventsPromise;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderAddLink = () => (
|
||||
<Anchor className="add-link" to="/admin/events/create">
|
||||
<img src={AddIcon} /> Create event
|
||||
</Anchor>
|
||||
)
|
||||
|
||||
renderData = () => {
|
||||
const { events, error } = this.state;
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
return <div>No events.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Start time</th>
|
||||
<th>End time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map(event => (
|
||||
<tr key={event.id}>
|
||||
<td><Anchor to={`${URL}/${event.id}`}>{event.title_fi}</Anchor></td>
|
||||
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map(event => (
|
||||
<tr key={event.id}>
|
||||
<td><Anchor to={`/admin/events/${event.id}`}>{event.title_fi}</Anchor></td>
|
||||
<td>{formatRelative(new Date(event.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(event.end_time), new Date())}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
const AdminEventPage: React.FC = () => {
|
||||
const [events, setEvents] = useState<Event[]>(null);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-event-page">
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/events" />
|
||||
</Helmet>
|
||||
<h1>Events</h1>
|
||||
{this.renderAddLink()}
|
||||
{this.renderData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
getEvents({ auth: true })
|
||||
.then(res => setEvents(res))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<Helmet>
|
||||
<link rel="canonical" href={`https://sik.ayy.fi/${URL}`} />
|
||||
</Helmet>
|
||||
<h1>Feed</h1>
|
||||
<AddLink text="Create event" to={`${URL}/create`} />
|
||||
{renderData(events)}
|
||||
</AdminListCommon>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminEventPage;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
|
||||
.admin-feed-page {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid color(white1);
|
||||
padding: 0.5rem;
|
||||
|
||||
a {
|
||||
color: color(orange1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +1,58 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import Anchor from "@components/Anchor";
|
||||
import "./AdminFeedPage.scss";
|
||||
import { StaticContext } from "@server/StaticContext";
|
||||
import { Post, getFeed } from "@models/Feed";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { th } from "date-fns/esm/locale";
|
||||
import AddIcon from "@assets/img/add-icon.png";
|
||||
import AdminListCommon from "./AdminListCommon";
|
||||
import Anchor from "@components/Anchor";
|
||||
import AddLink from "@components/AddLink";
|
||||
import { Post, getFeed } from "@models/Feed";
|
||||
|
||||
export interface AdminFeedPageProps {
|
||||
staticContext: StaticContext;
|
||||
}
|
||||
export interface AdminFeedPageState {
|
||||
feed: Post[];
|
||||
error?: string;
|
||||
}
|
||||
const URL = "/admin/feed"
|
||||
|
||||
class AdminFeedPage extends React.Component<AdminFeedPageProps, AdminFeedPageState> {
|
||||
constructor(props: AdminFeedPageProps) {
|
||||
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.getFeed) {
|
||||
const feed = staticContext.resolutions.getFeed as Post[];
|
||||
this.state = {
|
||||
feed,
|
||||
};
|
||||
} else {
|
||||
this.state = {
|
||||
feed: [],
|
||||
};
|
||||
const promiseFeed = this.fetchFeed();
|
||||
staticContext.promises.getFeed = promiseFeed;
|
||||
}
|
||||
} else {
|
||||
this.state = {
|
||||
feed: [],
|
||||
};
|
||||
this.fetchFeed();
|
||||
}
|
||||
const renderData = (feed: Post[]) => {
|
||||
if (!feed || feed.length === 0) {
|
||||
return <div>No posts.</div>;
|
||||
}
|
||||
|
||||
fetchFeed = async () => {
|
||||
const getFeedPromise = getFeed();
|
||||
try {
|
||||
const feed = await getFeedPromise;
|
||||
this.setState({
|
||||
feed,
|
||||
});
|
||||
return getFeedPromise;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderAddLink = () => (
|
||||
<Anchor className="add-link" to="/admin/feed/create">
|
||||
<img src={AddIcon} /> Create post
|
||||
</Anchor>
|
||||
)
|
||||
|
||||
renderData = () => {
|
||||
const { feed, error } = this.state;
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (!feed || feed.length === 0) {
|
||||
return <div>No posts.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Publish time</th>
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Publish time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feed.map(post => (
|
||||
<tr key={post.id}>
|
||||
<td><Anchor to={`${URL}/${post.id}`}>{post.title_fi}</Anchor></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feed.map(post => (
|
||||
<tr key={post.id}>
|
||||
<td><Anchor to={`/admin/feed/${post.id}`}>{post.title_fi}</Anchor></td>
|
||||
<td>{post.description_fi}</td>
|
||||
<td>{formatRelative(new Date(post.publish_time), new Date())}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-feed-page">
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/feed" />
|
||||
</Helmet>
|
||||
<h1>Feed</h1>
|
||||
{this.renderAddLink()}
|
||||
{this.renderData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const AdminFeedPage: React.FC = () => {
|
||||
const [forms, setForms] = useState<Post[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getFeed({ auth: true })
|
||||
.then(res => setForms(res))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<Helmet>
|
||||
<link rel="canonical" href={`https://sik.ayy.fi/${URL}`} />
|
||||
</Helmet>
|
||||
<h1>Feed</h1>
|
||||
<AddLink text="Create news post" to={`${URL}/create`} />
|
||||
{renderData(forms)}
|
||||
</AdminListCommon>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminFeedPage;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.admin-front-page {
|
||||
a {
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import Anchor from "@components/Anchor";
|
||||
import "./AdminFrontPage.scss";
|
||||
|
||||
export interface AdminFrontPageProps { }
|
||||
export interface AdminFrontPageState { }
|
||||
|
||||
class AdminFrontPage extends React.Component<AdminFrontPageProps, AdminFrontPageState> {
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-front-page">
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin" />
|
||||
</Helmet>
|
||||
<h1>SIK Admin</h1>
|
||||
<Anchor to="/admin/events">Events</Anchor>
|
||||
<Anchor to="/admin/feed">Feed</Anchor>
|
||||
<Anchor to="/admin/jobads">Job advertisements</Anchor>
|
||||
<Anchor to="https:https://static.sika.sik.party/admin">Files</Anchor>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminFrontPage;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
const AdminFrontView: React.FC = () => (
|
||||
<main data-e2e="admin-front-page">
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin" />
|
||||
</Helmet>
|
||||
<h1>SIK Admin</h1>
|
||||
</main>
|
||||
);
|
||||
|
||||
export default AdminFrontView;
|
||||
@@ -1,19 +1,14 @@
|
||||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { formatRelative } from "date-fns";
|
||||
import AdminListCommon from "./AdminListCommon";
|
||||
import Anchor from "@components/Anchor";
|
||||
import AddLink from "@components/AddLink";
|
||||
import useFetchJobAds from "@hooks/useFetchJobAds";
|
||||
import { JobAd } from "@models/JobAd";
|
||||
import Anchor from "@components/Anchor";
|
||||
import AddIcon from "@assets/img/add-icon.png";
|
||||
|
||||
const URL = "/admin/jobads"
|
||||
|
||||
const renderAddLink = () => (
|
||||
<Anchor className="add-link" to={`${URL}/create`}>
|
||||
<img src={AddIcon} /> Create Ad
|
||||
</Anchor>
|
||||
)
|
||||
|
||||
const renderData = (jobAds: JobAd[]) => {
|
||||
|
||||
if (!jobAds || jobAds.length === 0) {
|
||||
@@ -49,14 +44,14 @@ const renderData = (jobAds: JobAd[]) => {
|
||||
const AdminJobAdPage: React.FC = () => {
|
||||
const jobAds = useFetchJobAds({ auth: true });
|
||||
return (
|
||||
<div>
|
||||
<AdminListCommon>
|
||||
<Helmet>
|
||||
<link rel="canonical" href={`https://sik.ayy.fi/${URL}`} />
|
||||
</Helmet>
|
||||
<h1>Job advertisements</h1>
|
||||
{renderAddLink()}
|
||||
<AddLink text="Create job ad" to={`${URL}/create`} />
|
||||
{renderData(jobAds)}
|
||||
</div>
|
||||
</AdminListCommon>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from "styled-components";
|
||||
import { colors } from "@theme/colors";
|
||||
|
||||
const Main = styled.div`
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid ${colors.white};
|
||||
padding: 0.5rem;
|
||||
|
||||
a {
|
||||
color: ${colors.orange1};
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Main;
|
||||
@@ -1,5 +0,0 @@
|
||||
.admin-login-page {
|
||||
input {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import qs from "query-string";
|
||||
import { generateToken, setTokenCookie, isAuthenticated } from "@utils/auth";
|
||||
import "./AdminLoginPage.scss";
|
||||
|
||||
export interface AdminLoginPageProps {
|
||||
const Main = styled.div`
|
||||
input {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
interface AdminLoginPageProps {
|
||||
history: {
|
||||
push: (to: string | string[]) => void;
|
||||
};
|
||||
@@ -13,7 +19,7 @@ export interface AdminLoginPageProps {
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
export interface AdminLoginPageState {
|
||||
interface AdminLoginPageState {
|
||||
username: string;
|
||||
password: string;
|
||||
isAuthenticated: boolean;
|
||||
@@ -104,7 +110,7 @@ class AdminLoginPage extends React.Component<AdminLoginPageProps, AdminLoginPage
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-login-page">
|
||||
<Main>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/login" />
|
||||
</Helmet>
|
||||
@@ -122,7 +128,7 @@ class AdminLoginPage extends React.Component<AdminLoginPageProps, AdminLoginPage
|
||||
<input id="login-submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
{ this.renderError() }
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
.signup-create-page {
|
||||
a {
|
||||
color: color(orange1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-signup-page {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid color(white1);
|
||||
padding: 0.5rem;
|
||||
|
||||
a {
|
||||
color: color(orange1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +1,58 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { formatRelative } from "date-fns";
|
||||
import AdminListCommon from "./AdminListCommon";
|
||||
import Anchor from "@components/Anchor";
|
||||
|
||||
import "./AdminSignupPage.scss";
|
||||
import AddLink from "@components/AddLink";
|
||||
import { SignupForm, getForms } from "@models/SignupForm";
|
||||
import { StaticContext } from "@server/StaticContext";
|
||||
import AddIcon from "@assets/img/add-icon.png";
|
||||
|
||||
export interface AdminSignupPageProps {
|
||||
staticContext: StaticContext;
|
||||
}
|
||||
export interface AdminSignupPageState {
|
||||
signupForms: SignupForm[];
|
||||
error?: string;
|
||||
}
|
||||
const URL = "/admin/signups"
|
||||
|
||||
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();
|
||||
}
|
||||
const renderData = (signupForms: SignupForm[]) => {
|
||||
if (!signupForms || signupForms.length === 0) {
|
||||
return <div>No signup forms.</div>;
|
||||
}
|
||||
|
||||
fetchSignupForms = async () => {
|
||||
const getSignupFormsPromise = getForms(true);
|
||||
try {
|
||||
const signupForms = await getSignupFormsPromise;
|
||||
this.setState({
|
||||
signupForms,
|
||||
});
|
||||
return getSignupFormsPromise;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderAddLink = () => (
|
||||
<Anchor className="add-link" to="/admin/signups/create">
|
||||
<img src={AddIcon} /> Create signup form
|
||||
</Anchor>
|
||||
)
|
||||
|
||||
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>
|
||||
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><Anchor to={`${URL}/${signupForm.id}`}>{signupForm.title_fi}</Anchor></td>
|
||||
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{signupForms.map(signupForm => (
|
||||
<tr key={signupForm.id}>
|
||||
<td><Anchor to={`/admin/signups/${signupForm.id}`}>{signupForm.title_fi}</Anchor></td>
|
||||
<td>{formatRelative(new Date(signupForm.start_time), new Date())}</td>
|
||||
<td>{formatRelative(new Date(signupForm.end_time), new Date())}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
))}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
const AdminSignupPage: React.FC = () => {
|
||||
const [forms, setForms] = useState<SignupForm[]>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getForms(true)
|
||||
.then(res => setForms(res))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AdminListCommon>
|
||||
<Helmet>
|
||||
<link rel="canonical" href={`https://sik.ayy.fi/${URL}`} />
|
||||
</Helmet>
|
||||
<h1>Sign-up forms</h1>
|
||||
<AddLink text="Create signup form" to={`${URL}/create`} />
|
||||
{renderData(forms)}
|
||||
</AdminListCommon>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminSignupPage;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
@import "../../assets/scss/globals";
|
||||
|
||||
|
||||
.event-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 {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: color(blue1);
|
||||
color: color(white1);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
-285
@@ -1,103 +1,199 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import "./EventCreatePage.scss";
|
||||
import Form from "react-jsonschema-form";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import AdminCreateCommon from "./AdminCreateCommon";
|
||||
import { Tag, getTags } from "@models/Tag";
|
||||
import { SignupForm, getForms } from "@models/SignupForm";
|
||||
import { createEvent, getEvent, updateEvent, Event } from "@models/Event";
|
||||
import { Event, createEvent, getEvent, updateEvent } from "@models/Event";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget/DatetimeWidget";
|
||||
import SectionDividerWidget from "@components/Widgets/SectionDividerWidget/SectionDividerWidget";
|
||||
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
|
||||
|
||||
|
||||
const widgets = {
|
||||
datetime: DatetimeWidget,
|
||||
section_divider: SectionDividerWidget,
|
||||
markdownEditor: MarkdownEditorWidget
|
||||
};
|
||||
|
||||
export interface EventCreatePageProps {
|
||||
history: {
|
||||
push: (to: string) => void;
|
||||
const buildSchema = (formData: Event, signupForms: SignupForm[], tags: Tag[]) => {
|
||||
const date = new Date(), tomorrowDate = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
||||
const tomorrowDatetime = tomorrowDate.toISOString();
|
||||
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Event",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
title: "Event tags",
|
||||
items: {
|
||||
type: "number",
|
||||
enum: tags.map(t => t.id),
|
||||
enumNames: tags.map(t => t.name_fi),
|
||||
},
|
||||
uniqueItems: true,
|
||||
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,
|
||||
},
|
||||
signupForm: {
|
||||
type: "array",
|
||||
title: "Signup forms",
|
||||
items: {
|
||||
type: "number",
|
||||
// TODO: A bug here, DB must have at least one SignupForm, otherwise cannot submit
|
||||
enum: signupForms.map(form => form.id),
|
||||
enumNames: signupForms.map(form => form.title_fi),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
image: {
|
||||
type: ["string", "null"],
|
||||
format: formData?.image ? "uri-reference" : "data-url",
|
||||
title: "Override tag icon with image",
|
||||
default: undefined
|
||||
},
|
||||
finnish_section_divider: {
|
||||
title: "Finnish",
|
||||
type: "string",
|
||||
},
|
||||
title_fi: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_fi: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_fi: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
location_fi: {
|
||||
type: "string",
|
||||
title: "Location",
|
||||
default: "",
|
||||
},
|
||||
english_section_divider: {
|
||||
title: "English",
|
||||
type: "string",
|
||||
},
|
||||
title_en: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_en: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_en: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
location_en: {
|
||||
type: "string",
|
||||
title: "Location",
|
||||
default: "",
|
||||
},
|
||||
}
|
||||
};
|
||||
match: {
|
||||
params: {
|
||||
id?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface EventCreatePageState {
|
||||
tags: Tag[];
|
||||
signupForm: SignupForm[];
|
||||
error?: string;
|
||||
statusMessage?: string;
|
||||
formData: any;
|
||||
return schema;
|
||||
}
|
||||
|
||||
class EventCreatePage extends React.Component<EventCreatePageProps, EventCreatePageState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tags: [],
|
||||
signupForm: [],
|
||||
formData: {},
|
||||
};
|
||||
const buildUISchema = () => {
|
||||
const uiSchema = {
|
||||
content_fi: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
content_en: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
start_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
end_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
image: {
|
||||
"ui:options": {
|
||||
accept: [".jpg", ".jpeg", ".png"]
|
||||
}
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
english_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
};
|
||||
return uiSchema;
|
||||
}
|
||||
|
||||
this.fetchTags();
|
||||
this.fetchSignupForms();
|
||||
interface MatchParams {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const { id } = props.match.params;
|
||||
if (id !== undefined) {
|
||||
this.fetchInitialFormData(id);
|
||||
type EventCreatePageProps = RouteComponentProps<MatchParams>;
|
||||
|
||||
const EventCreatePage: React.FC<EventCreatePageProps> = ({ match: { params: { id } } }) => {
|
||||
const [formData, setFormData] = useState<Event>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [signupForms, setSignupForms] = useState<SignupForm[]>([]);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
getTags()
|
||||
.then(res => setTags(res))
|
||||
.catch(err => setError(err))
|
||||
|
||||
getForms(true)
|
||||
.then(res => setSignupForms(res))
|
||||
.catch(err => setError(err))
|
||||
|
||||
const eventId = id && Number(id);
|
||||
if (eventId !== undefined) {
|
||||
getEvent(eventId, true)
|
||||
.then(res => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map(inst => inst.id) as any,
|
||||
signupForm: (res.signupForm).map(inst => inst.id) as any,
|
||||
}))
|
||||
.catch(err => setError(err))
|
||||
}
|
||||
}
|
||||
}, [id])
|
||||
|
||||
fetchInitialFormData = async (id) => {
|
||||
try {
|
||||
const data = await getEvent(id, true);
|
||||
data.tags = (data.tags as any).map(inst => inst.id);
|
||||
data.signupForm = (data.signupForm as any).map(inst => inst.id);
|
||||
this.setState({
|
||||
formData: data,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchTags = async () => {
|
||||
try {
|
||||
const tags = await getTags();
|
||||
this.setState({
|
||||
tags,
|
||||
});
|
||||
return tags;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
fetchSignupForms = async () => {
|
||||
try {
|
||||
const signupForm = await getForms(true);
|
||||
this.setState({
|
||||
signupForm
|
||||
})
|
||||
return signupForm;
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = async (data) => {
|
||||
const { history } = this.props;
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const payload = data.formData;
|
||||
payload.signup_id = payload.signupForm;
|
||||
@@ -112,10 +208,8 @@ class EventCreatePage extends React.Component<EventCreatePageProps, EventCreateP
|
||||
// resp.signupForm = (resp.signupForm as any).map(inst => inst.id);
|
||||
resp.tags = data.formData.tags;
|
||||
resp.signupForm = data.formData.signupForm;
|
||||
this.setState({
|
||||
formData: resp,
|
||||
statusMessage: "Event created successfully",
|
||||
});
|
||||
setStatusMessage("Event created successfully");
|
||||
setFormData(resp);
|
||||
} else {
|
||||
const resp = await updateEvent(payload);
|
||||
// TODO: Backend return old data because of Prefetch (used for filtering invisble signupForms from GET)
|
||||
@@ -124,213 +218,39 @@ class EventCreatePage extends React.Component<EventCreatePageProps, EventCreateP
|
||||
// resp.signupForm = (resp.signupForm as any).map(inst => inst.id);
|
||||
resp.tags = data.formData.tags;
|
||||
resp.signupForm = data.formData.signupForm;
|
||||
this.setState({
|
||||
formData: resp,
|
||||
statusMessage: "Event updated successfully",
|
||||
});
|
||||
setStatusMessage("Event updated successfully");
|
||||
setFormData(resp);
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
|
||||
onError = (data) => {
|
||||
console.error("error, data:");
|
||||
console.log(this.state.formData);
|
||||
console.log(data);
|
||||
}
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
const onFocus = () => setStatusMessage(null);
|
||||
const title = formData?.id
|
||||
? `Edit Event "${formData.title_fi}"`
|
||||
: "Create Event";
|
||||
|
||||
onChange = (data) => {
|
||||
this.setState({
|
||||
formData: data.formData,
|
||||
});
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
statusMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
buildSchema = () => {
|
||||
const { tags, signupForm, error } = this.state;
|
||||
|
||||
const formData = this.state.formData as Event;
|
||||
|
||||
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_fi : "New Event",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "tags", "location_fi", "location_en", "start_time", "end_time", "description_fi", "description_en", "content_fi", "content_en"],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
title: "Event tags",
|
||||
items: {
|
||||
type: "number",
|
||||
enum: tags.map(t => t.id),
|
||||
enumNames: tags.map(t => t.name_fi),
|
||||
},
|
||||
uniqueItems: true,
|
||||
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,
|
||||
},
|
||||
signupForm: {
|
||||
type: "array",
|
||||
title: "Signup forms",
|
||||
items: {
|
||||
type: "number",
|
||||
// TODO: A bug here, DB must have at least one SignupForm, otherwise cannot submit
|
||||
enum: signupForm.map(form => form.id),
|
||||
enumNames: signupForm.map(form => form.title_fi),
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
image: {
|
||||
type: ["string", "null"],
|
||||
format: formData.image ? "uri-reference" : "data-url",
|
||||
title: "Override tag icon with image",
|
||||
default: undefined
|
||||
},
|
||||
finnish_section_divider: {
|
||||
title: "Finnish",
|
||||
type: "string",
|
||||
},
|
||||
title_fi: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_fi: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_fi: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
location_fi: {
|
||||
type: "string",
|
||||
title: "Location",
|
||||
default: "",
|
||||
},
|
||||
english_section_divider: {
|
||||
title: "English",
|
||||
type: "string",
|
||||
},
|
||||
title_en: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_en: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_en: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
location_en: {
|
||||
type: "string",
|
||||
title: "Location",
|
||||
default: "",
|
||||
},
|
||||
}
|
||||
};
|
||||
return schema;
|
||||
}
|
||||
|
||||
buildUISchema = () => {
|
||||
const uiSchema = {
|
||||
content_fi: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
content_en: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
start_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
end_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
image: {
|
||||
"ui:options": {
|
||||
accept: [".jpg", ".jpeg", ".png"]
|
||||
}
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
english_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
};
|
||||
return uiSchema;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, statusMessage } = this.state;
|
||||
const formData = this.state.formData as Event;
|
||||
const schema = this.buildSchema();
|
||||
const uiSchema = this.buildUISchema();
|
||||
|
||||
const title = formData.id
|
||||
? `Edit Event "${formData.title_fi}"`
|
||||
: "Create Event";
|
||||
|
||||
return (
|
||||
<div className="event-create-page">
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/events/create" />
|
||||
</Helmet>
|
||||
<h1>{title}</h1>
|
||||
{statusMessage && <div className="success">{statusMessage}</div>}
|
||||
<Form schema={schema as any}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
idPrefix="rjsf"
|
||||
widgets={widgets as any}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onSubmit}
|
||||
onError={this.onError}
|
||||
onFocus={this.onFocus} />
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/events/create" />
|
||||
</Helmet>
|
||||
<AdminCreateCommon
|
||||
title={title}
|
||||
formData={formData}
|
||||
schema={buildSchema(formData, signupForms, tags)}
|
||||
UISchema={buildUISchema()}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onSubmit={onSubmit}
|
||||
statusMessage={statusMessage}
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventCreatePage;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
@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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: color(blue1);
|
||||
color: color(white1);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
-190
@@ -1,230 +1,209 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import Form from "react-jsonschema-form";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import "./FeedCreatePage.scss";
|
||||
import AdminCreateCommon from "./AdminCreateCommon";
|
||||
import { Tag, getTags } from "@models/Tag";
|
||||
import { createPost, getPost, updatePost } from "@models/Feed";
|
||||
import { Post, createPost, getPost, updatePost } from "@models/Feed";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget/DatetimeWidget";
|
||||
import SectionDividerWidget from "@components/Widgets/SectionDividerWidget/SectionDividerWidget";
|
||||
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
|
||||
|
||||
const widgets = {
|
||||
datetime: DatetimeWidget,
|
||||
section_divider: SectionDividerWidget,
|
||||
markdownEditor: MarkdownEditorWidget
|
||||
};
|
||||
|
||||
const buildSchema = (formData: Post, tags: Tag[]) => {
|
||||
const date = new Date();
|
||||
const currentDatetime = date.toISOString();
|
||||
|
||||
const schema = {
|
||||
title: formData?.title_fi ?? "New Post",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "publish_time"],
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
title: "Post tags",
|
||||
items: {
|
||||
type: "number",
|
||||
enum: tags.map(t => t.id),
|
||||
enumNames: tags.map(t => t.name_fi),
|
||||
},
|
||||
uniqueItems: true,
|
||||
default: [],
|
||||
},
|
||||
visible: {
|
||||
type: "boolean",
|
||||
title: "Visible",
|
||||
default: true,
|
||||
},
|
||||
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: "",
|
||||
},
|
||||
finnish_section_divider: {
|
||||
title: "Finnish",
|
||||
type: "string",
|
||||
},
|
||||
title_fi: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_fi: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_fi: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
english_section_divider: {
|
||||
title: "English",
|
||||
type: "string",
|
||||
},
|
||||
title_en: {
|
||||
type: "string",
|
||||
title: "Title",
|
||||
default: ""
|
||||
},
|
||||
description_en: {
|
||||
type: "string",
|
||||
title: "Description",
|
||||
default: "",
|
||||
},
|
||||
content_en: {
|
||||
type: "string",
|
||||
title: "Content",
|
||||
default: "",
|
||||
},
|
||||
}
|
||||
};
|
||||
return schema;
|
||||
}
|
||||
|
||||
const buildUISchema = (formData: Post) => {
|
||||
const uiSchema = {
|
||||
content_fi: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
content_en: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
publish_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
autohide: {
|
||||
"ui:widget": formData && formData.autohide_enabled ? "datetime" : "hidden",
|
||||
},
|
||||
finnish_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
english_section_divider: {
|
||||
"ui:widget": "section_divider",
|
||||
"ui:options": {
|
||||
label: false
|
||||
},
|
||||
},
|
||||
};
|
||||
return uiSchema;
|
||||
}
|
||||
|
||||
interface MatchParams {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
type FeedCreatePageProps = RouteComponentProps<MatchParams>;
|
||||
|
||||
export interface FeedCreatePageState {
|
||||
tags: Tag[];
|
||||
error?: string;
|
||||
statusMessage?: string;
|
||||
formData: any;
|
||||
}
|
||||
const FeedCreatePage: React.FC<FeedCreatePageProps> = ({ match: { params: { id } } }) => {
|
||||
const [formData, setFormData] = useState<Post>(null);
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(null);
|
||||
|
||||
class FeedCreatePage extends React.Component<FeedCreatePageProps, FeedCreatePageState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tags: [],
|
||||
formData: {},
|
||||
};
|
||||
useEffect(() => {
|
||||
|
||||
this.fetchTags();
|
||||
getTags()
|
||||
.then(res => setTags(res))
|
||||
.catch(err => setError(err))
|
||||
|
||||
const {id} = props.match.params;
|
||||
if (id !== undefined) {
|
||||
this.fetchInitialFormData(id);
|
||||
const feedId = id && Number(id);
|
||||
if (feedId !== undefined) {
|
||||
getPost(feedId)
|
||||
// getPost(feedId, true)
|
||||
.then(res => setFormData({
|
||||
...res,
|
||||
tags: (res.tags).map(inst => inst.id) as any,
|
||||
}))
|
||||
.catch(err => setError(err))
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}, [id])
|
||||
|
||||
const onSubmit = async (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.tags;
|
||||
this.setState({
|
||||
formData: resp,
|
||||
statusMessage: "Post created successfully",
|
||||
});
|
||||
setStatusMessage("Post created successfully");
|
||||
setFormData(resp);
|
||||
} else {
|
||||
const resp = await updatePost(payload);
|
||||
// resp.tags = resp.tag_id;
|
||||
this.setState({
|
||||
formData: resp,
|
||||
statusMessage: "Post updated successfully.",
|
||||
});
|
||||
setStatusMessage("Post updated successfully");
|
||||
setFormData(resp);
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
|
||||
onError = (data) => {
|
||||
console.error("error, data:");
|
||||
console.log(data);
|
||||
}
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
const onFocus = () => setStatusMessage(null);
|
||||
|
||||
onChange = (data) => {
|
||||
this.setState({
|
||||
formData: data.formData,
|
||||
});
|
||||
}
|
||||
const title = formData?.id
|
||||
? `Edit Post "${formData.title_fi}"`
|
||||
: "Create Post";
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
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_fi),
|
||||
},
|
||||
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 as any}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
idPrefix="rjsf"
|
||||
widgets={widgets as any}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onSubmit}
|
||||
onError={this.onError}
|
||||
onFocus={this.onFocus} />
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/feed/create" />
|
||||
</Helmet>
|
||||
<AdminCreateCommon
|
||||
title={title}
|
||||
formData={formData}
|
||||
schema={buildSchema(formData, tags)}
|
||||
UISchema={buildUISchema(formData)}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onSubmit={onSubmit}
|
||||
statusMessage={statusMessage}
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedCreatePage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import Form from "react-jsonschema-form";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import AdminCreateCommon from "./AdminCreateCommon";
|
||||
import { JobAd, getJobAd, createJobAd, updateJobAd } from "@models/JobAd";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget/DatetimeWidget";
|
||||
import SectionDividerWidget from "@components/Widgets/SectionDividerWidget/SectionDividerWidget";
|
||||
@@ -13,13 +13,12 @@ const widgets = {
|
||||
markdownEditor: MarkdownEditorWidget
|
||||
};
|
||||
|
||||
const buildSchema = (title: string) => {
|
||||
const date = new Date();
|
||||
const buildSchema = (formData: JobAd) => {
|
||||
const monthFromNow = new Date();
|
||||
monthFromNow.setDate(date.getDate() + 30);
|
||||
monthFromNow.setDate(new Date().getDate() + 30);
|
||||
|
||||
const schema = {
|
||||
title,
|
||||
title: formData?.title_fi ?? "New Job Ad",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"],
|
||||
properties: {
|
||||
@@ -105,8 +104,6 @@ const buildUISchema = (formData: JobAd) => ({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
interface MatchParams {
|
||||
id?: string;
|
||||
}
|
||||
@@ -114,21 +111,19 @@ interface MatchParams {
|
||||
type JobAdCreatePageProps = RouteComponentProps<MatchParams>;
|
||||
|
||||
const JobAdCreatePage: React.FC<JobAdCreatePageProps> = ({ match: { params: { id } } }) => {
|
||||
|
||||
const [formData, setFormData] = useState<JobAd>(null);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(null);
|
||||
useEffect(() => {
|
||||
const jobId = id && Number(id);
|
||||
if (jobId !== undefined) {
|
||||
getJobAd(jobId, true)
|
||||
.then(res => setFormData(res))
|
||||
.catch(err => setError(err))
|
||||
}
|
||||
}, [id])
|
||||
|
||||
|
||||
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(null);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const payload = data.formData;
|
||||
@@ -146,38 +141,31 @@ const JobAdCreatePage: React.FC<JobAdCreatePageProps> = ({ match: { params: { id
|
||||
}
|
||||
}
|
||||
|
||||
const onError = (data) => {
|
||||
console.error("error, data:");
|
||||
console.log(data);
|
||||
}
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
const onFocus = () => setStatusMessage(null);
|
||||
|
||||
|
||||
|
||||
const title = formData?.id
|
||||
? `Edit Ad "${formData.title_fi}"`
|
||||
: "Create Ad";
|
||||
|
||||
return (
|
||||
<div className="post-create-page">
|
||||
<>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/jobads/create" />
|
||||
</Helmet>
|
||||
<h1>{title}</h1>
|
||||
{statusMessage && <div className="success">{statusMessage}</div>}
|
||||
<Form
|
||||
schema={buildSchema(formData?.id ? formData.title_fi : "New Post") as any}
|
||||
uiSchema={buildUISchema(formData)}
|
||||
<AdminCreateCommon
|
||||
title={title}
|
||||
formData={formData}
|
||||
idPrefix="rjsf"
|
||||
widgets={widgets as any}
|
||||
schema={buildSchema(formData)}
|
||||
UISchema={buildUISchema(formData)}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onSubmit={onSubmit}
|
||||
onError={onError}
|
||||
onFocus={onFocus} />
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
statusMessage={statusMessage}
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
@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 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: color(blue1);
|
||||
color: color(white1);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
label {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,126 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Link } from "react-router-dom";
|
||||
import "./SignupCreatePage.scss";
|
||||
import Form from "react-jsonschema-form";
|
||||
import { createForm, getForm, updateForm, SignupForm } from "@models/SignupForm";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import AdminCreateCommon from "./AdminCreateCommon";
|
||||
import { SignupForm, createForm, getForm, updateForm } from "@models/SignupForm";
|
||||
import DatetimeWidget from "@components/Widgets/DatetimeWidget/DatetimeWidget";
|
||||
import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget";
|
||||
import MarkdownEditorWidget from "@components/Widgets/MarkdownEditorWidget";
|
||||
import { buildValidationSchema } from "@views/SignUpPage/FormUtils";
|
||||
|
||||
const DEFAULT_EMAIL =
|
||||
`Moikka,
|
||||
|
||||
Ilmottautuminen saapui perille.`
|
||||
;
|
||||
|
||||
const widgets = {
|
||||
datetime: DatetimeWidget,
|
||||
signup: SignupQuestionsWidget,
|
||||
markdownEditor: MarkdownEditorWidget
|
||||
};
|
||||
const DEFAULT_EMAIL =
|
||||
`Moikka,
|
||||
|
||||
Ilmottautuminen saapui perille.`
|
||||
;
|
||||
export interface SignupCreatePageProps {
|
||||
history: {
|
||||
push: (to: string) => void;
|
||||
const buildSchema = (formData: SignupForm) => {
|
||||
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?.title_fi ?? "New Sign-up form",
|
||||
type: "object",
|
||||
required: ["title_fi", "title_en", "start_time", "end_time", "questions"],
|
||||
properties: {
|
||||
title_fi: {
|
||||
type: "string",
|
||||
title: "Title (FI)",
|
||||
default: "",
|
||||
},
|
||||
title_en: {
|
||||
type: "string",
|
||||
title: "Title (EN)",
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: "boolean",
|
||||
title: "Visible",
|
||||
default: false,
|
||||
},
|
||||
quota: {
|
||||
type: "integer",
|
||||
title: "Quota",
|
||||
minimum: 0,
|
||||
default: 0,
|
||||
},
|
||||
start_time: {
|
||||
type: "string",
|
||||
title: "Start time",
|
||||
default: currentDatetime,
|
||||
},
|
||||
end_time: {
|
||||
type: "string",
|
||||
title: "End time",
|
||||
default: tomorrowDatetime,
|
||||
},
|
||||
email_content: {
|
||||
type: "string",
|
||||
title: "Email on signup",
|
||||
default: DEFAULT_EMAIL
|
||||
},
|
||||
questions: {
|
||||
type: "string",
|
||||
title: "Questions",
|
||||
default: "[]",
|
||||
},
|
||||
},
|
||||
};
|
||||
match: {
|
||||
params: {
|
||||
id?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export interface SignupCreatePageState {
|
||||
error?: string;
|
||||
statusMessage?: string;
|
||||
formData: any;
|
||||
return schema;
|
||||
}
|
||||
|
||||
class SignupCreatePage extends React.Component<SignupCreatePageProps, SignupCreatePageState> {
|
||||
constructor(props: SignupCreatePageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
formData: {},
|
||||
};
|
||||
const buildUISchema = () => {
|
||||
const uiSchema = {
|
||||
email_content: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
start_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
end_time: {
|
||||
"ui:widget": "datetime",
|
||||
},
|
||||
questions: {
|
||||
"ui:widget": "signup",
|
||||
},
|
||||
};
|
||||
return uiSchema;
|
||||
}
|
||||
|
||||
const {id} = props.match.params;
|
||||
if (id !== undefined) {
|
||||
this.fetchInitialFormData(id);
|
||||
interface MatchParams {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
type SignupCreatePageProps = RouteComponentProps<MatchParams>;
|
||||
|
||||
const SignupCreatePage: React.FC<SignupCreatePageProps> = ({ match: { params: { id } } }) => {
|
||||
const [formData, setFormData] = useState<SignupForm>(null);
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [statusMessage, setStatusMessage] = useState<string>(null);
|
||||
useEffect(() => {
|
||||
const suId = id && Number(id);
|
||||
if (suId !== undefined) {
|
||||
getForm(suId, true)
|
||||
.then(res => {
|
||||
setFormData({
|
||||
...res,
|
||||
questions: JSON.stringify(res.questions) as any
|
||||
});
|
||||
})
|
||||
.catch(err => setError(err))
|
||||
}
|
||||
}
|
||||
}, [id])
|
||||
|
||||
fetchInitialFormData = async (id: number) => {
|
||||
try {
|
||||
const data = await getForm(id, true);
|
||||
this.setState({
|
||||
formData: {
|
||||
...data,
|
||||
questions: JSON.stringify(data.questions)
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = async (data: any) => {
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
const questions = JSON.parse(data.formData.questions);
|
||||
const payload: SignupForm = {
|
||||
@@ -75,158 +131,53 @@ class SignupCreatePage extends React.Component<SignupCreatePageProps, SignupCrea
|
||||
|
||||
if (payload.id === undefined) {
|
||||
const resp = await createForm(payload);
|
||||
this.setState({
|
||||
formData: {
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions)
|
||||
},
|
||||
statusMessage: "Sign-up created successfully",
|
||||
setStatusMessage("Sign-up created successfully");
|
||||
setFormData({
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions) as any
|
||||
});
|
||||
} else {
|
||||
const resp = await updateForm(payload);
|
||||
this.setState({
|
||||
formData: {
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions)
|
||||
},
|
||||
statusMessage: "Sign-up updated successfully.",
|
||||
setStatusMessage("Sign-up updated successfully");
|
||||
setFormData({
|
||||
...resp,
|
||||
questions: JSON.stringify(resp.questions) as any
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error: error,
|
||||
statusMessage: error.message
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
}
|
||||
}
|
||||
|
||||
onError = (data) => {
|
||||
console.error("error, data:");
|
||||
console.log(data);
|
||||
}
|
||||
const onChange = (data) => setFormData(data.formData);
|
||||
const onFocus = () => setStatusMessage(null);
|
||||
|
||||
onChange = (data) => {
|
||||
this.setState({
|
||||
formData: data.formData,
|
||||
});
|
||||
}
|
||||
const title = formData?.id
|
||||
? `Edit Sign-up Form "${formData.title_fi}"`
|
||||
: "Create Sign-up form";
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
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_fi", "title_en", "start_time", "end_time", "questions"],
|
||||
properties: {
|
||||
title_fi: {
|
||||
type: "string",
|
||||
title: "Title (FI)",
|
||||
default: "",
|
||||
},
|
||||
title_en: {
|
||||
type: "string",
|
||||
title: "Title (EN)",
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: "boolean",
|
||||
title: "Visible",
|
||||
default: false,
|
||||
},
|
||||
quota: {
|
||||
type: "integer",
|
||||
title: "Quota",
|
||||
minimum: 0,
|
||||
default: 0,
|
||||
},
|
||||
start_time: {
|
||||
type: "string",
|
||||
title: "Start time",
|
||||
default: currentDatetime,
|
||||
},
|
||||
end_time: {
|
||||
type: "string",
|
||||
title: "End time",
|
||||
default: tomorrowDatetime,
|
||||
},
|
||||
email_content: {
|
||||
type: "string",
|
||||
title: "Email on signup",
|
||||
default: DEFAULT_EMAIL
|
||||
},
|
||||
questions: {
|
||||
type: "string",
|
||||
title: "Questions",
|
||||
default: "[]",
|
||||
},
|
||||
},
|
||||
};
|
||||
return schema;
|
||||
}
|
||||
|
||||
buildUISchema = () => {
|
||||
const uiSchema = {
|
||||
email_content: {
|
||||
"ui:widget": "markdownEditor",
|
||||
},
|
||||
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>}
|
||||
{formData.id && <p>
|
||||
Check out the signup form here: <Link to={`/signup/${formData.id}`}>{formData.title}</Link>
|
||||
</p>}
|
||||
<Form schema={schema as any}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
idPrefix="rjsf"
|
||||
widgets={widgets as any}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onSubmit}
|
||||
onError={this.onError}
|
||||
onFocus={this.onFocus} />
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<link rel="canonical" href="https://sik.ayy.fi/admin/signups/create" />
|
||||
</Helmet>
|
||||
<AdminCreateCommon
|
||||
title={title}
|
||||
formData={formData}
|
||||
schema={buildSchema(formData)}
|
||||
UISchema={buildUISchema()}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onSubmit={onSubmit}
|
||||
statusMessage={statusMessage}
|
||||
error={error}
|
||||
widgets={widgets}
|
||||
/>
|
||||
{/* {formData.id && <p>
|
||||
Check out the signup form here: <Link to={`/signup/${formData.id}`}>{formData.title_fi}</Link>
|
||||
</p>} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignupCreatePage;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Switch, Route, Redirect } from "react-router-dom";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import FrontPage from "./pages/FrontPage";
|
||||
import GuildPage from "./pages/GuildPage";
|
||||
@@ -7,7 +7,7 @@ import NotFoundPage from "./pages/NotFoundPage";
|
||||
import CommonPage from "./pages/CommonPage";
|
||||
import JsonLD from "@components/JsonLD";
|
||||
import "./index.scss";
|
||||
import AdminFrontPage from "./pages/admin/AdminFrontPage";
|
||||
import AdminFrontPage from "./pages/admin/AdminFrontView";
|
||||
import AdminEventPage from "./pages/admin/AdminEventPage";
|
||||
import AdminFeedPage from "./pages/admin/AdminFeedPage";
|
||||
import AdminCommonPage from "./pages/admin/AdminCommonPage";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => {};
|
||||
export default noop;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { Hero, HeroPrimarySection, HeroSecondarySection, HeroSecondarySectionItem, HeroAside, HeroAsideItem, HeroPrimaryButtons } from "@components/Hero";
|
||||
import Anchor from "@components/Anchor";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
const ActualPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
@@ -11,12 +12,12 @@ const ActualPageHero: React.FC = () => (
|
||||
>
|
||||
<HeroPrimaryButtons row>
|
||||
<Anchor to="#tapahtumat">
|
||||
<button onClick={() => { }}>
|
||||
<button onClick={noop}>
|
||||
<span>Tapahtumat ›</span>
|
||||
</button>
|
||||
</Anchor>
|
||||
<Anchor to="#uutiset">
|
||||
<button onClick={() => { }}>
|
||||
<button onClick={noop}>
|
||||
<span>Uutiset ›</span>
|
||||
</button>
|
||||
</Anchor>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Button from "@components/Button";
|
||||
|
||||
import FilterContainer from "./FilterContainer";
|
||||
import { CardSection, Card, FullWidthSection } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
interface EventCalendarProps {
|
||||
events: Event[];
|
||||
@@ -38,7 +39,7 @@ const EventCalendar: React.FC<EventCalendarProps> = ({events}) => {
|
||||
start_time={e.start_time}
|
||||
text={e.description_fi}
|
||||
link={`/events/${e.id}`}
|
||||
buttonOnClick={() => {}}
|
||||
buttonOnClick={noop}
|
||||
>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Button from "@components/Button";
|
||||
|
||||
import FilterContainer from "./FilterContainer";
|
||||
import { CardSection, Card, FullWidthSection } from "@components/index";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
interface NewsProps {
|
||||
feed: Post[];
|
||||
@@ -37,7 +38,7 @@ const News: React.FC<NewsProps> = ({feed}) => {
|
||||
start_time={post.publish_time}
|
||||
text={post.description_fi}
|
||||
link={`/feed/${post.id}`}
|
||||
buttonOnClick={() => {}}
|
||||
buttonOnClick={noop}
|
||||
>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { colors } from "@theme/colors";
|
||||
import { Event } from "@models/Event";
|
||||
import Button from "@components/Button";
|
||||
import Anchor from "@components/Anchor";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
interface EventPageViewProps {
|
||||
event?: Event;
|
||||
@@ -91,7 +92,7 @@ const EventPageView: React.FC<EventPageViewProps> = ({ event }) => {
|
||||
<SignupButtons>
|
||||
{event.signupForm.map(sf => (
|
||||
<Anchor key={sf.id} to={`/signup/${sf.id}`}>
|
||||
<Button type="filled" onClick={() => {}}>
|
||||
<Button type="filled" onClick={noop}>
|
||||
{sf.title_fi}
|
||||
</Button>
|
||||
</Anchor>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import Anchor from "@components/Anchor";
|
||||
import { Hero, HeroPrimarySection, HeroAside, HeroAsideItem, HeroPrimaryButtons } from "@components/Hero";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
const FrontPageHero: React.FC = () => (
|
||||
<Hero>
|
||||
@@ -11,12 +12,12 @@ const FrontPageHero: React.FC = () => (
|
||||
>
|
||||
<HeroPrimaryButtons>
|
||||
<Anchor to="/kilta">
|
||||
<button onClick={() => { }}>
|
||||
<button onClick={noop}>
|
||||
<span>Tietoa killasta ›</span>
|
||||
</button>
|
||||
</Anchor>
|
||||
<Anchor to="/kilta/toiminta">
|
||||
<button onClick={() => { }}>
|
||||
<button onClick={noop}>
|
||||
<span>Vastapainoa opiskelulle ›</span>
|
||||
</button>
|
||||
</Anchor>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Post } from "@models/Feed";
|
||||
import { colors } from "@theme/colors";
|
||||
import Anchor from "@components/Anchor";
|
||||
import FullWidthSection from "@components/Sections/FullWidthSection";
|
||||
import noop from "@utils/noop";
|
||||
|
||||
interface FrontPageViewProps {
|
||||
events: Event[];
|
||||
@@ -51,7 +52,7 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
text={event.description_fi}
|
||||
link={`/events/${event.id}`}
|
||||
image={event.image || event.tags[0].icon}
|
||||
buttonOnClick={() => {}}
|
||||
buttonOnClick={noop}
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
@@ -78,7 +79,7 @@ const FrontPageView: React.FC<FrontPageViewProps> = ({ events, feed }) => (
|
||||
start_time={inst.publish_time}
|
||||
text={inst.description_fi}
|
||||
link={`/feed/${inst.id}`}
|
||||
buttonOnClick={() => {}}
|
||||
buttonOnClick={noop}
|
||||
/>
|
||||
))}
|
||||
<aside>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { SignupForm } from "@models/SignupForm";
|
||||
import { EMAIL_REGEX } from "@utils/regexes";
|
||||
import escapeRegExp from "lodash/escapeRegExp";
|
||||
|
||||
const questionToUISchemaProp = (question: Question): {} => {
|
||||
let obj = {};
|
||||
const questionToUISchemaProp = (question: Question) => {
|
||||
let obj: Record<"ui:widget", string>;
|
||||
if (question.type == "checkbox") {
|
||||
obj = {
|
||||
"ui:widget": "checkboxes",
|
||||
@@ -16,16 +16,16 @@ const questionToUISchemaProp = (question: Question): {} => {
|
||||
"ui:widget": "radio",
|
||||
}
|
||||
}
|
||||
// else {
|
||||
// throw new Error(`No mapping to UI schema prop for question type ${question.type}`);
|
||||
// }
|
||||
else {
|
||||
throw new Error(`No mapping to UI schema prop for question type ${question.type}`);
|
||||
}
|
||||
return {
|
||||
[question.id]: obj,
|
||||
};
|
||||
}
|
||||
|
||||
const questionToValidationSchema = (question: Question) => {
|
||||
let obj = {};
|
||||
let obj: Record<string, unknown>;
|
||||
if (question.type === "text" || question.type === "name") {
|
||||
obj = {
|
||||
type: "string",
|
||||
|
||||
@@ -82,7 +82,7 @@ const StudiesPageView: React.FC = () => (
|
||||
|
||||
|
||||
<TextSection>
|
||||
<h3 className="large">Sähkötekniikan korkeakoulun toimikunnat</h3>
|
||||
<h3>Sähkötekniikan korkeakoulun toimikunnat</h3>
|
||||
<div>
|
||||
<p>
|
||||
Ylioppilaskunnalla on edustus suuressa osassa Aalto-yliopiston tiedekuntia. Elektroniikan, tietoliikenteen ja automaation tiedekunnan toimikuntiin eli hallopedeihin opiskelijavalinna EST- ja TLT-tutkinto-ohjelmien osalta tehdään osin ylioppilaskunnan edustajistossa Sähköinsinöörikillan esityksestä ja osin Sähköinsinöörikillassa.
|
||||
|
||||
@@ -26,7 +26,7 @@ test("User can log in with default credentials", async t => {
|
||||
await t.typeText(Selector("#login-password"), PASSWORD);
|
||||
await t.click(Selector("#login-submit"));
|
||||
|
||||
const frontPage = Selector(".admin-front-page");
|
||||
const frontPage = Selector(`[data-e2e="admin-front-page"]`);
|
||||
await t.expect(frontPage.exists).ok();
|
||||
});
|
||||
|
||||
@@ -47,11 +47,10 @@ test("User is redirected to login when JWT token is invalid", async t => {
|
||||
/**
|
||||
* Test if the user is redirected to login when JWT token is invalid.
|
||||
*/
|
||||
const TOKEN = "FOOBAR";
|
||||
const setCookie = ClientFunction(() => {
|
||||
document.cookie = `jwt=${TOKEN}`;
|
||||
document.cookie = "jwt=FOOBAR";
|
||||
});
|
||||
|
||||
await setCookie();
|
||||
await t.navigateTo("/admin/events");
|
||||
|
||||
const loginForm = Selector("form.admin-login-form");
|
||||
|
||||
Reference in New Issue
Block a user