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:
Aarni Halinen
2020-11-24 21:22:56 +00:00
49 changed files with 3117 additions and 2168 deletions
+1990 -587
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -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",
+35
View File
@@ -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;
+2 -3
View File
@@ -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>
+34 -10
View File
@@ -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&nbsp;</StyledLink>
<StyledLink to="/admin/events" path={path}>Events&nbsp;</StyledLink>
<StyledLink to="/admin/feed" path={path}>Feed&nbsp;</StyledLink>
<StyledLink to="/admin/signups" path={path}>Signup forms&nbsp;</StyledLink>
<StyledLink to="/admin/jobads" path={path}>Job advertisements&nbsp;</StyledLink>
<StyledLink to="https://static.sika.sik.party/admin" path={path}>Files&nbsp;</StyledLink>
<StyledLink id="admin-sidebar-logout" to="/admin/logout" path={path}>Logout&nbsp;</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;
-2
View File
@@ -1,2 +0,0 @@
import AdminSidebarLink from "./AdminSidebarLink";
export default AdminSidebarLink;
+10 -14
View File
@@ -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}&nbsp;{getIconByLabel(label)}
</h3>
);
}
}
const SectionDividerWidget: React.FC<SectionDividerWidgetProps> = ({ label }) => (
<h3 className="section-divider-widget">
{label}&nbsp;{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;
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
-79
View File
@@ -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;
}
}
}
+31 -3
View File
@@ -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>
</>
);
}
+164
View File
@@ -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;
-20
View File
@@ -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;
}
}
}
+45 -106
View File
@@ -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;
-20
View File
@@ -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;
}
}
}
+47 -106
View File
@@ -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;
-6
View File
@@ -1,6 +0,0 @@
.admin-front-page {
a {
text-decoration: underline;
display: block;
}
}
-26
View File
@@ -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;
+13
View File
@@ -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;
+6 -11
View File
@@ -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>
)
}
+22
View File
@@ -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;
-5
View File
@@ -1,5 +0,0 @@
.admin-login-page {
input {
display: block;
}
}
+11 -5
View File
@@ -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>
);
}
}
-26
View File
@@ -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;
}
}
}
+45 -104
View File
@@ -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;
-55
View File
@@ -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
View File
@@ -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;
-55
View File
@@ -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
View File
@@ -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;
+18 -30
View File
@@ -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}
/>
</>
);
}
-55
View File
@@ -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;
}
}
}
}
+142 -191
View File
@@ -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
View File
@@ -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";
+3
View File
@@ -0,0 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
export default noop;
+3 -2
View File
@@ -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&nbsp;</span>
</button>
</Anchor>
<Anchor to="#uutiset">
<button onClick={() => { }}>
<button onClick={noop}>
<span>Uutiset&nbsp;</span>
</button>
</Anchor>
+2 -1
View File
@@ -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>
))}
+2 -1
View File
@@ -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>
))}
+2 -1
View File
@@ -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>
+3 -2
View File
@@ -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&nbsp;</span>
</button>
</Anchor>
<Anchor to="/kilta/toiminta">
<button onClick={() => { }}>
<button onClick={noop}>
<span>Vastapainoa opiskelulle&nbsp;</span>
</button>
</Anchor>
+3 -2
View File
@@ -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>
+6 -6
View File
@@ -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",
+1 -1
View File
@@ -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.
+3 -4
View File
@@ -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");