Commit fe307eb6 authored by Alex Aisting's avatar Alex Aisting
Browse files

* Cloned from develop

parent c07031f1
Pipeline #782 failed
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
{
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"prettier",
"prettier/react"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react/react-in-jsx-scope": 0, // Prevent missing React when using JSX: No need in Next.js since React is global.
"react/prop-types": 0
},
"plugins": ["react", "import", "jsx-a11y", "react-hooks"],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"settings": {
"react": {
"version": "detect"
}
}
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
stages:
- build
- deploy
include:
- project: "contact-tracing/dev-ops"
file: "/gitlab/appweb/.gitlab-ci-image-build.yml"
- project: "contact-tracing/dev-ops"
file: "/gitlab/appweb/.gitlab-ci-deploy.yml"
\ No newline at end of file
{}
\ No newline at end of file
# Builder - based on Node.js, to build and compile the frontend
FROM docker.repo.tehik.ee/node:12.18.1-alpine as builder
RUN mkdir -p /opt/webpage
COPY package.json /opt/webpage
WORKDIR /opt/webpage
RUN npm install
COPY . /opt/webpage
RUN npm run export
# RUN - have only the compiled app, ready for production with Nginx
FROM docker.repo.tehik.ee/nginx:1.19
COPY --from=builder /opt/webpage/out /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# It is very important to set the WORKDIR to the directory of the file to be exectued after the ENTRYPOINT script or reference it absolutely
WORKDIR /etc/nginx
# Define the command that should be executed at the container startup
# For Docker containers the daemon off; directive tells Nginx to stay in the foreground. For containers this is useful as best practice is for
# one container = one process. One server (container) has only one service.
CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
# app-web
# HOIA website
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). Build to be multilingual and to use [YAML](https://yaml.org/) files for content.
## Getting Started
**Install dependencies:**
```bash
npm install
```
**Run the development server:**
```bash
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Content
All content is in [YAML](https://yaml.org/) files in the `content` directory.
Content is separated into two types and every language has their own file:
- `content.*.yaml` – general content
- `faq.*.yaml` – FAQ section questions and answers
## Build and Deploymet
Staging environment uses [Vercel Platform](https://vercel.com/)
Check out [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details on various deployment ways.
**Static HTML build**
For a static HTML export run:
```bash
npm run export
```
The build system will generate directory named `out`, which can be served by any static hosting service or CDN.
import ReactMarkdown from "react-markdown/with-html";
export default function AppFeatures({ content }) {
return (
<div className="space-y-12 md:space-y-0 md:flex md:space-x-8 lg:space-x-16 xl:space-x-24">
{content.main.features.map((item) => {
return (
<div key={item.icon} className="md:flex-1">
<img
className="mx-auto mb-6"
src={`/images/icon-${item.icon}.svg`}
alt=""
/>
<h2 className="text-2xl font-bold leading-tight mb-4 text-center">
{item.title}
</h2>
<ReactMarkdown
escapeHtml={false}
source={item.content}
className="body-copy"
/>
</div>
);
})}
</div>
);
}
import ReactMarkdown from "react-markdown/with-html";
export default function AppShare({ content }) {
return (
<div className="text-center">
<h2 className="text-2xl font-bold leading-tight mb-4 text-center">
{content.main.social_media.title}
</h2>
<ReactMarkdown
escapeHtml={false}
source={content.main.social_media.content}
className="mb-4 body-copy"
/>
<div className="flex space-x-5 justify-center font-bold">
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://hoia.me"
className="flex items-center space-x-2"
>
<img src="/images/icon-facebook.svg" alt="" /> <span>Facebook</span>
</a>
<a
href="https://twitter.com/home?status=https://hoia.me "
className="flex items-center space-x-2"
>
<img src="/images/icon-twitter.svg" alt="" />
<span>Twitter</span>
</a>
</div>
</div>
);
}
import Head from "next/head";
import PageHeader from "./PageHeader";
import PageFooter from "./PageFooter";
import ReactMarkdown from "react-markdown/with-html";
import { useRouter } from "next/router";
export default function ContentPage({ content, locale }) {
return (
<>
<style global jsx>{`
html {
// Override scroll behavior on privacy page
scroll-behavior: auto !important;
}
`}</style>
<Head>
<title>{content.main.seo.title}</title>
</Head>
<PageHeader content={content} locale={locale} />
<div className="wrap mb-32 pt-10">
<ReactMarkdown
escapeHtml={false}
source={content.page}
className="body-copy"
/>
</div>
<PageFooter content={content} locale={locale} />
</>
);
}
import ReactMarkdown from "react-markdown/with-html";
import {
Accordion,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from "react-accessible-accordion";
export default function FaqQuestionsGroup({ group }) {
return (
<div className="card mb-5">
<h3 className="text-blue font-bold text-2xl mb-4">{group.heading}</h3>
<Accordion allowMultipleExpanded={true} allowZeroExpanded={true}>
{group.questions.map((item) => {
return (
<AccordionItem key={item.q} className="mb-5 max-w-5xl">
<AccordionItemHeading className="font-bold mb-2" aria-level={4}>
<AccordionItemButton
dangerouslySetInnerHTML={{ __html: item.q }}
></AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
<ReactMarkdown
escapeHtml={false}
source={item.a}
className="body-copy"
/>
</AccordionItemPanel>
</AccordionItem>
);
})}
</Accordion>
</div>
);
}
import ReactMarkdown from "react-markdown/with-html";
import FaqQuestionsGroup from "./FaqQuestionsGroup";
import SectionTitle from "./SectionTitle";
export default function FaqSection({ content }) {
return (
<>
<SectionTitle title={content.main.faq.title} icon="question-mark" />
{content.faq.map((group) => {
return <FaqQuestionsGroup key={group.heading} group={group} />;
})}
<div className="text-center mt-24">
<h2 className="text-2xl font-bold leading-tight mb-2 text-center">
{content.main.faq.more_info.title}
</h2>
<ReactMarkdown
escapeHtml={false}
source={content.main.faq.more_info.content}
className="body-copy"
/>
</div>
</>
);
}
import Head from "next/head";
import PageHeader from "./PageHeader";
import FaqSection from "./FaqSection";
import PageHero from "./PageHero";
import AppFeatures from "./AppFeatures";
import AppShare from "./AppShare";
import HowItWorks from "./HowItWorks";
import PageFooter from "./PageFooter";
import PageConsortium from "./PageConsortium";
import slug from "slug";
export default function HomePage({ content, locale }) {
return (
<>
<Head>
<title>{content.main.seo.title}</title>
</Head>
<PageHeader content={content} locale={locale} />
<main>
<div className="mb-16 md:mb-20 lg:mb-24 xl:mb-32">
<PageHero content={content} locale={locale} />
</div>
<div className="wrap mb-16 md:mb-20 lg:mb-24 xl:mb-32">
<div className="mb-20">
<AppFeatures content={content} />
</div>
<AppShare content={content} />
</div>
<div
className="bg-gray py-16 md:py-20 lg:py-24 xl:py-32 mb-16 md:mb-20 lg:mb-24 xl:mb-32"
id={slug(content.main.how.title)}
>
<div className="wrap">
<HowItWorks content={content} />
<div
className="pt-16 md:pt-20 lg:pt-24 xl:pt-32"
id={slug(content.main.faq.title)}
>
<FaqSection content={content} />
</div>
</div>
</div>
<div
className="wrap mb-16 md:mb-20 lg:mb-24 xl:mb-32"
id={slug(content.main.consortium.title)}
>
<PageConsortium content={content} />
</div>
</main>
<PageFooter content={content} locale={locale} />
</>
);
}
import SectionTitle from "./SectionTitle";
import ReactMarkdown from "react-markdown/with-html";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "../tailwind.config.js";
import { useRef } from "react";
const tw = resolveConfig(tailwindConfig);
export default function HowItWorks({ content }) {
const refContainer = useRef(null);
return (
<>
<SectionTitle title={content.main.how.title} icon="info" />
<OverlayScrollbarsComponent ref={refContainer} options={{}}>
<div
className="flex flex-col items-center max-w-7xl mx-auto pb-6"
style={{ minWidth: tw.theme.screens.md, minHeight: "100px" }}
>
<img
className="w-full mb-10 flex-shrink-0"
src="/images/illustration-how-it-works.svg"
alt=""
/>
<ol className="flex space-x-5 text-sm w-full">
{content.main.how.steps.map((step) => {
return (
<li className="flex-1" key={step}>
<ReactMarkdown escapeHtml={false} source={step} />
</li>
);
})}
</ol>
</div>
</OverlayScrollbarsComponent>
<div className="md:hidden flex justify-between py-4">
<button
onClick={() => {
refContainer.current.osInstance().scrollStop().scroll(
{
x: "-= 50%",
},
100,
"linear"
);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="33"
height="33"
>
<title>{content.main.scroll_left}</title>
<line
x1="23"
y1="12"
x2="6"
y2="12"
fill="none"
stroke="#0000f0"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
<polyline
points="8 19 1 12 8 5"
fill="none"
stroke="#0000f0"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
</button>
<button
onClick={() => {
refContainer.current.osInstance().scrollStop().scroll(
{
x: "+= 50%",
},
100,
"linear"
);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="33"
height="33"
>
<title>{content.main.scroll_right}</title>
<line
x1="1"
y1="12"
x2="18"
y2="12"
fill="none"
stroke="#0000f0"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
<polyline
points="16 5 23 12 16 19"
fill="none"
stroke="#0000f0"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
</button>
</div>
</>
);
}
import Link from "next/link";
import { useState } from "react";
export default function LanguageSelector({ locale }) {
const [languages] = useState([
{
code: "et",
name: "eesti keeles",
},
{
code: "en",
name: "in english",
},
{
code: "ru",
name: "по русски",
},
]);
const itemClassName = "block py-1 px-2 focus:text-blue hover:text-blue";
return (
<div className="flex items-center">
<img className="mr-1" src="/images/icon-globe.svg" alt="" />
<ul className="flex leading-6 text-xs">
{languages.map((language) => {
if (language.code === locale) {
null;
} else if (language.code === "et") {
return (
<li key={language.code}>
<Link as={`/`} href={`/`}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
hrefLang={language.code}
onClick={() => {
document.documentElement.lang = language.code;
}}
className={itemClassName}
>
{language.name}
</a>
</Link>
</li>
);
} else {
return (
<li key={language.code}>
<Link as={`/${language.code}/`} href={`/[lang]`}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
hrefLang={language.code}
onClick={() => {
document.documentElement.lang = language.code;
}}
className={itemClassName}
>
{language.name}
</a>
</Link>
</li>
);
}
})}
</ul>
</div>
);
}
This source diff could not be displayed because it is too large. You can view the blob instead.
import ReactMarkdown from "react-markdown/with-html";
import Link from "next/link";
export default function PageFooter({ content, locale }) {
return (
<footer className="bg-blue text-white pt-6 pb-16 sm:pb-24 sm:pt-24">
<div className="wrap">
<div className="flex sm:justify-between flex-col-reverse sm:flex-row">
<div>
<address className="not-italic mb-5">
<h2 className="font-bold mb-3">{content.main.footer.heading}</h2>
<ReactMarkdown
escapeHtml={false}
source={content.main.footer.content}
className="body-copy body-copy--dark text-sm"
/>
</address>
{locale !== "et" ? (
<Link as={`/${locale}/privacy`} href={`/[lang]/privacy`}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a className="text-sm underline">
{content.main.footer.privacy}
</a>
</Link>
) : (
<Link href="/privacy">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a className="text-sm underline">
{content.main.footer.privacy}