From 22c0ed0d1c14ddfe53c8085e3487421b55bc8859 Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Sat, 7 Nov 2020 19:42:48 +0000 Subject: [PATCH] Feature: Create, list & edit job advertisements --- .../{AdminHeader => }/AdminHeader.tsx | 0 src/components/AdminHeader/index.ts | 2 - src/components/AdminSidebar.tsx | 33 ++++ src/components/AdminSidebar/AdminSidebar.scss | 13 -- src/components/AdminSidebar/AdminSidebar.tsx | 26 --- src/components/AdminSidebar/index.ts | 2 - src/hooks/useFetchJobAd.ts | 15 ++ src/hooks/useFetchJobAds.ts | 15 ++ src/models/JobAd.ts | 83 ++++++++ src/pages/CorporatePage.tsx | 25 ++- src/pages/admin/AdminFeedPage.tsx | 1 - src/pages/admin/AdminFrontPage.tsx | 1 + src/pages/admin/AdminJobAdPage.tsx | 63 ++++++ src/pages/admin/FeedCreatePage.tsx | 18 +- src/pages/admin/JobAdCreatePage.tsx | 183 ++++++++++++++++++ src/routes.tsx | 7 + src/views/CorporatePage/CorporatePageView.tsx | 9 +- src/views/CorporatePage/JobAdList.tsx | 29 +++ src/views/CorporatePage/JobAdView.tsx | 92 +++++++++ tsconfig.json | 3 + 20 files changed, 550 insertions(+), 70 deletions(-) rename src/components/{AdminHeader => }/AdminHeader.tsx (100%) delete mode 100644 src/components/AdminHeader/index.ts create mode 100644 src/components/AdminSidebar.tsx delete mode 100644 src/components/AdminSidebar/AdminSidebar.scss delete mode 100644 src/components/AdminSidebar/AdminSidebar.tsx delete mode 100644 src/components/AdminSidebar/index.ts create mode 100644 src/hooks/useFetchJobAd.ts create mode 100644 src/hooks/useFetchJobAds.ts create mode 100644 src/models/JobAd.ts create mode 100644 src/pages/admin/AdminJobAdPage.tsx create mode 100644 src/pages/admin/JobAdCreatePage.tsx create mode 100644 src/views/CorporatePage/JobAdList.tsx create mode 100644 src/views/CorporatePage/JobAdView.tsx diff --git a/src/components/AdminHeader/AdminHeader.tsx b/src/components/AdminHeader.tsx similarity index 100% rename from src/components/AdminHeader/AdminHeader.tsx rename to src/components/AdminHeader.tsx diff --git a/src/components/AdminHeader/index.ts b/src/components/AdminHeader/index.ts deleted file mode 100644 index 8c087b1..0000000 --- a/src/components/AdminHeader/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import AdminHeader from "./AdminHeader"; -export default AdminHeader; diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx new file mode 100644 index 0000000..3dffe0a --- /dev/null +++ b/src/components/AdminSidebar.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styled from "styled-components"; +import AdminSidebarLink from "./AdminSidebarLink"; + +interface AdminSidebarProps { + path: string; +} + +const SideBar = styled.nav` + display: flex; + flex-flow: column nowrap; + align-self: stretch; + margin-right: 1rem; + + @media screen and (max-width: 800px - 1px) { + margin-right: 0; + margin-bottom: 1rem; + } +`; + +const AdminSidebar: React.FC = ({ path }) => ( + + Home + Events + Feed + Signup forms + Job advertisements + Files + Logout + +); + +export default AdminSidebar; diff --git a/src/components/AdminSidebar/AdminSidebar.scss b/src/components/AdminSidebar/AdminSidebar.scss deleted file mode 100644 index 30037c7..0000000 --- a/src/components/AdminSidebar/AdminSidebar.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import "../../assets/scss/globals"; - -.admin-sidebar { - display: flex; - flex-flow: column nowrap; - align-self: stretch; - margin-right: 1rem; - - @media screen and (max-width: 800px - 1px) { - margin-right: 0; - margin-bottom: 1rem; - } -} diff --git a/src/components/AdminSidebar/AdminSidebar.tsx b/src/components/AdminSidebar/AdminSidebar.tsx deleted file mode 100644 index ef84fe7..0000000 --- a/src/components/AdminSidebar/AdminSidebar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import "./AdminSidebar.scss"; -import AdminSidebarLink from "../AdminSidebarLink"; - -export interface AdminSidebarProps { - path: string; -} -export interface AdminSidebarState {} - -class AdminSidebar extends React.Component { - render() { - const { path } = this.props; - return ( -
- Home - Events - Feed - Signup forms - Files - Logout -
- ); - } -} - -export default AdminSidebar; diff --git a/src/components/AdminSidebar/index.ts b/src/components/AdminSidebar/index.ts deleted file mode 100644 index a4ca9be..0000000 --- a/src/components/AdminSidebar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import AdminSidebar from "./AdminSidebar"; -export default AdminSidebar; diff --git a/src/hooks/useFetchJobAd.ts b/src/hooks/useFetchJobAd.ts new file mode 100644 index 0000000..a5cb1bb --- /dev/null +++ b/src/hooks/useFetchJobAd.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; +import { JobAd, getJobAd } from "@models/JobAd"; + +const useFetchJobAd = (id: number) => { + const [jobAd, setJobAd] = useState(null); + + useEffect(() => { + getJobAd(id) + .then(res => setJobAd(res)) + }, []); + + return jobAd; +} + +export default useFetchJobAd; diff --git a/src/hooks/useFetchJobAds.ts b/src/hooks/useFetchJobAds.ts new file mode 100644 index 0000000..6111add --- /dev/null +++ b/src/hooks/useFetchJobAds.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; +import { JobAd, getJobAds, GetJobAdsOptions } from "@models/JobAd"; + +const useFetchJobAds = (options: GetJobAdsOptions = {}) => { + const [jobAds, setJobAds] = useState([]); + + useEffect(() => { + getJobAds(options) + .then(res => setJobAds(res)) + }, []); + + return jobAds; +} + +export default useFetchJobAds; diff --git a/src/models/JobAd.ts b/src/models/JobAd.ts new file mode 100644 index 0000000..784e6e9 --- /dev/null +++ b/src/models/JobAd.ts @@ -0,0 +1,83 @@ +import axios from "axios"; +import qs from "query-string"; +import { getAuthHeader } from "@utils/auth"; +const url = `${process.env.API_URL}/jobads/`; + +export interface JobAd { + id: number; + title_fi: string; + title_en: string; + description_fi: string; + description_en: string; + content_fi: string; + content_en: string; + autohide_at: Date; + autohide_enabled: boolean; +} + +export interface GetJobAdsOptions { + onlyNonPast?: boolean; + limit?: number; + auth?: boolean; +} + +export const getJobAds = async (options: GetJobAdsOptions = {}): Promise => { + const { onlyNonPast, limit, auth } = options; + try { + const params = { + since: onlyNonPast ? (new Date()).toISOString() : undefined, + limit, + }; + const search = qs.stringify(params); + const headers = auth ? { "Authorization": getAuthHeader() } : null; + const resp = await axios.get(`${url}?${search}`, { + headers + }); + return resp.data["results"]; + } catch (err) { + console.error(err); + throw err; + } +} + +export const getJobAd = async (id: number, auth = false): Promise => { + try { + const headers = auth ? { "Authorization": getAuthHeader() } : null; + const resp = await axios.get(`${url}${id}/`, { + headers + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} + +export const createJobAd = async (data: any): Promise => { + try { + const resp = await axios.post(url, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} + +export const updateJobAd = async (data: any): Promise => { + try { + const putUrl = `${url}${data.id}/`; + const resp = await axios.put(putUrl, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} \ No newline at end of file diff --git a/src/pages/CorporatePage.tsx b/src/pages/CorporatePage.tsx index 80c9035..4ec19c1 100644 --- a/src/pages/CorporatePage.tsx +++ b/src/pages/CorporatePage.tsx @@ -1,21 +1,18 @@ import React from "react"; import { Helmet } from "react-helmet"; import CorporatePageView from "@views/CorporatePage/CorporatePageView"; +import useFetchJobAds from "@hooks/useFetchJobAds"; -export interface CorporatePageProps {} -export interface CorporatePageState {} - -class CorporatePage extends React.Component { - render() { - return ( - <> - - - - - - ); - } +const CorporatePage: React.FC = () => { + const jobAds = useFetchJobAds(); + return ( + <> + + + + + + ); } export default CorporatePage; diff --git a/src/pages/admin/AdminFeedPage.tsx b/src/pages/admin/AdminFeedPage.tsx index a72ac1e..d8c0257 100644 --- a/src/pages/admin/AdminFeedPage.tsx +++ b/src/pages/admin/AdminFeedPage.tsx @@ -4,7 +4,6 @@ import Anchor from "@components/Anchor"; import "./AdminFeedPage.scss"; import { StaticContext } from "@server/StaticContext"; import { Post, getFeed } from "@models/Feed"; -import { getEvents } from "@models/Event"; import { formatRelative } from "date-fns"; import { th } from "date-fns/esm/locale"; import AddIcon from "@assets/img/add-icon.png"; diff --git a/src/pages/admin/AdminFrontPage.tsx b/src/pages/admin/AdminFrontPage.tsx index a111b10..2afe542 100644 --- a/src/pages/admin/AdminFrontPage.tsx +++ b/src/pages/admin/AdminFrontPage.tsx @@ -16,6 +16,7 @@ class AdminFrontPage extends React.ComponentSIK Admin Events Feed + Job advertisements Files ); diff --git a/src/pages/admin/AdminJobAdPage.tsx b/src/pages/admin/AdminJobAdPage.tsx new file mode 100644 index 0000000..6a0a926 --- /dev/null +++ b/src/pages/admin/AdminJobAdPage.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Helmet } from "react-helmet"; +import { formatRelative } from "date-fns"; +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 = () => ( + + Create Ad + +) + +const renderData = (jobAds: JobAd[]) => { + + if (!jobAds || jobAds.length === 0) { + return
No advertisements.
; + } + + return ( + + + + + + + + + + {jobAds.map(ad => ( + + + + + + ))} + +
TitleDescriptionAutohide
{ad.title_fi}{ad.description_fi} + {ad.autohide_enabled ? + formatRelative(new Date(ad.autohide_at), new Date()) + : "Disabled"} +
+ ); +} + +const AdminJobAdPage: React.FC = () => { + const jobAds = useFetchJobAds({ auth: true }); + return ( +
+ + + +

Job advertisements

+ {renderAddLink()} + {renderData(jobAds)} +
+ ) +} + +export default AdminJobAdPage; \ No newline at end of file diff --git a/src/pages/admin/FeedCreatePage.tsx b/src/pages/admin/FeedCreatePage.tsx index faff74b..58a022e 100644 --- a/src/pages/admin/FeedCreatePage.tsx +++ b/src/pages/admin/FeedCreatePage.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Helmet } from "react-helmet"; -import "./FeedCreatePage.scss"; -import { isAuthenticated } from "@utils/auth"; import Form from "react-jsonschema-form"; +import { RouteComponentProps } from "react-router-dom"; +import "./FeedCreatePage.scss"; import { Tag, getTags } from "@models/Tag"; import { createPost, getPost, updatePost } from "@models/Feed"; import DatetimeWidget from "@components/Widgets/DatetimeWidget/DatetimeWidget"; @@ -11,16 +11,12 @@ const widgets = { datetime: DatetimeWidget, }; -export interface FeedCreatePageProps { - history: { - push: (to: string) => void; - }; - match: { - params: { - id?: number; - }; - }; +interface MatchParams { + id?: string; } + +type FeedCreatePageProps = RouteComponentProps; + export interface FeedCreatePageState { tags: Tag[]; error?: string; diff --git a/src/pages/admin/JobAdCreatePage.tsx b/src/pages/admin/JobAdCreatePage.tsx new file mode 100644 index 0000000..e36f202 --- /dev/null +++ b/src/pages/admin/JobAdCreatePage.tsx @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from "react"; +import { Helmet } from "react-helmet"; +import Form from "react-jsonschema-form"; +import { RouteComponentProps } from "react-router-dom"; +import { JobAd, getJobAd, createJobAd, updateJobAd } from "@models/JobAd"; +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 = (title: string) => { + const date = new Date(); + const monthFromNow = new Date().setDate(date.getDate() + 30) + + const schema = { + title, + type: "object", + required: ["title_fi", "title_en", "description_fi", "description_en", "content_fi", "content_en", "autohide_at", "autohide_enabled", "visible"], + properties: { + visible: { + type: "boolean", + title: "Visible", + default: true, + }, + autohide_enabled: { + type: "boolean", + title: "Autohide enabled", + default: false, + }, + autohide_at: { + type: "string", + title: "Autohide time", + default: monthFromNow, + }, + 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: JobAd) => ({ + content_fi: { + "ui:widget": "markdownEditor", + }, + content_en: { + "ui:widget": "markdownEditor", + }, + autohide_at: { + "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 + }, + }, +}); + + + +interface MatchParams { + id?: string; +} + +type JobAdCreatePageProps = RouteComponentProps; + +const JobAdCreatePage: React.FC = ({ match: { params: { id } } }) => { + + const [formData, setFormData] = useState(null); + useEffect(() => { + const jobId = Number(id); + if (jobId !== undefined) { + getJobAd(jobId, true) + .then(res => setFormData(res)) + } + }, [id]) + + + + const [error, setError] = useState(null); + const [statusMessage, setStatusMessage] = useState(null); + + const onSubmit = async (data) => { + try { + const payload = data.formData; + if (payload.id === undefined) { + const resp = await createJobAd(payload); + setStatusMessage("Post created successfully"); + setFormData(resp); + } else { + const resp = await updateJobAd(payload); + setStatusMessage("Post updated successfully"); + setFormData(resp); + } + } catch (err) { + setError(err); + } + } + + 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 ( +
+ + + +

{title}

+ {statusMessage &&
{statusMessage}
} +
+ {error &&
{error}
} +
+ ); +} + +export default JobAdCreatePage; \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index 6a0e045..f3ff88f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -25,6 +25,9 @@ import StudiesPage from "./pages/StudiesPage"; import CorporatePage from "./pages/CorporatePage"; import InEnglishPage from "./pages/InEnglishPage"; import EventPage from "./pages/EventPage"; +import JobAd from "@views/CorporatePage/JobAdView"; +import AdminJobAdPage from "@pages/admin/AdminJobAdPage"; +import JobAdCreatePage from "@pages/admin/JobAdCreatePage"; const renderPage = (Page) => (props): JSX.Element => { return ; @@ -49,6 +52,7 @@ const commonRoutes = [ { path: "/opinnot_ja_ura", page: StudiesPage }, { path: "/yritysyhteistyo", page: CorporatePage }, { path: "/yhteystiedot", page: ContactsPage }, + { path: "/jobads/:id", page: JobAd }, { path: "/in_english", page: InEnglishPage }, ]; @@ -66,6 +70,9 @@ const adminRoutes = [ { path: "/admin/signups", page: AdminSignupPage }, { path: "/admin/signups/create", page: SignupCreatePage }, { path: "/admin/signups/:id", page: SignupCreatePage }, + { path: "/admin/jobads", page: AdminJobAdPage }, + { path: "/admin/jobads/create", page: JobAdCreatePage }, + { path: "/admin/jobads/:id", page: JobAdCreatePage }, { path: "/admin/logout", page: AdminLogoutPage }, { path: "/admin", page: AdminFrontPage }, ]; diff --git a/src/views/CorporatePage/CorporatePageView.tsx b/src/views/CorporatePage/CorporatePageView.tsx index 797bca7..6d22c00 100644 --- a/src/views/CorporatePage/CorporatePageView.tsx +++ b/src/views/CorporatePage/CorporatePageView.tsx @@ -1,8 +1,14 @@ import React from "react"; import CorporatePageHero from "./CorporatePageHero"; import { CTASection, TextSection, PageLink } from "@components/index"; +import { JobAd } from "@models/JobAd"; +import JobAdList from "./JobAdList"; -const CorporatePageView: React.FC = () => ( +interface CorporatePageViewProps { + jobAds: JobAd[]; +} + +const CorporatePageView: React.FC = ({ jobAds }) => ( <> @@ -66,6 +72,7 @@ const CorporatePageView: React.FC = () => (

Työpaikkailmoitukset

+

Voit saada yrityksesi työpaikkailmoituksen listalle lähettämällä sen osoitteeseen sik-yritys@list.ayy.fi

diff --git a/src/views/CorporatePage/JobAdList.tsx b/src/views/CorporatePage/JobAdList.tsx new file mode 100644 index 0000000..04479f0 --- /dev/null +++ b/src/views/CorporatePage/JobAdList.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import styled from "styled-components"; +import { JobAd } from "@models/JobAd"; +import Anchor from "@components/Anchor"; + +interface JobAdListProps { + jobAds: JobAd[]; +} + +const List = styled.ul` + li { + margin: 1rem 0; + } +`; + +const JobAdList: React.FC = ({ jobAds }) => ( + + {jobAds.map((ad) => ( +
  • + + {ad.title_fi} + +
  • + ))} +
    + +) + +export default JobAdList; diff --git a/src/views/CorporatePage/JobAdView.tsx b/src/views/CorporatePage/JobAdView.tsx new file mode 100644 index 0000000..be67277 --- /dev/null +++ b/src/views/CorporatePage/JobAdView.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import styled from "styled-components"; +import { Helmet } from "react-helmet"; +import { RouteComponentProps } from "react-router-dom"; +import ReactMarkdown from "react-markdown"; +import { colors } from "@theme/colors"; +import useFetchJobAd from "@hooks/useFetchJobAd"; + +const StyledSection = styled.section` + margin: 2rem auto; + max-width: 1000px; + align-items: center; + + & > h1 { + color: ${colors.darkBlue}; + } + + & > div > img { + height: auto; + width: 100%; + } + + & > p { + color: ${colors.orange1}; + } +`; + +const Content = styled.div` + margin-top: 24px; + + p { + color: ${colors.black}; + } + + h1, h3 { + color: ${colors.orange2}; + } + + a { + color: ${colors.blue1}; + + &:hover { + color: ${colors.lightBlue}; + } + } + + table { + tr { + vertical-align: top; + + td { + word-break: break-word; + padding: 8px; + } + + td:first-of-type { + word-break: unset; + padding-left: 0; + } + } + } +`; + + +interface MatchParams { + id: string; +} + +type JobAdProps = RouteComponentProps + +const JobAdView: React.FC = ({ match: { params: { id } } }) => { + const jobAd = useFetchJobAd(Number(id)); + if (!jobAd) return
    Loading
    + return ( + <> + + + + + +

    {jobAd.title_fi}

    +

    {jobAd.description_fi}

    + + + +
    + + + ); +} + +export default JobAdView; diff --git a/tsconfig.json b/tsconfig.json index 2739d2b..e4c1b47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,9 @@ "@components/*": [ "src/components/*" ], + "@hooks/*": [ + "src/hooks/*" + ], "@models/*": [ "src/models/*" ],