Compare commits

24 Commits

Author SHA1 Message Date
243db422b1 Dependency version bump 2022-08-03 01:00:04 +00:00
217f86e5af Add uploads page 2022-07-25 22:05:04 +00:00
91d16ced01 Add nav bar and fix spacing issues 2022-07-25 22:04:54 +00:00
4f20c073cc Remove useless redirect and do it right the first time 2022-07-25 21:41:34 +00:00
1065055cdb Payload config should load server url from env 2022-07-25 21:41:05 +00:00
7a6cc510a2 Copyfiles should copy pdfs 2022-07-25 21:39:56 +00:00
9c30d14975 Split start/end location fields to Events 2022-07-25 13:12:01 +00:00
4418ec2b82 Add pm2 config 2022-07-25 04:13:12 +00:00
e260756ce9 Fix .env file importing 2022-07-25 04:12:48 +00:00
af4f6f6f16 Style cleanup 2022-07-25 04:12:35 +00:00
d342799127 Added uploads section to single event page 2022-07-25 03:14:40 +00:00
b323e3b70a Add Uploads collection 2022-07-25 03:12:31 +00:00
4aae9867ef Lint fixes 2022-07-25 00:53:35 +00:00
262689e140 Add location and notes to Events collection 2022-07-25 00:52:59 +00:00
37e77d3405 Add single event page 2022-07-25 00:52:59 +00:00
5cdf43c845 Add favicon to homepage and make logo link to index 2022-07-25 00:52:59 +00:00
91ae70b190 Renamed script.js -> index.js 2022-07-25 00:52:59 +00:00
b504d3235e Add favicon files 2022-07-25 00:52:59 +00:00
1b52f6bea3 Setup lint for browser and js files 2022-07-25 00:52:59 +00:00
909cd08275 Add homepage to show all events 2022-07-24 21:55:16 +00:00
fa6e949805 Generate hsl for EventType label 2022-07-24 21:54:12 +00:00
4b4c1d5a1d Make Events and EventTypes collections world readable 2022-07-24 21:53:20 +00:00
88ead4ff03 Add tailwind 2022-07-24 21:50:03 +00:00
6b6eacf45d Rewrite events collection to use Text fields instead of Date 2022-07-24 13:41:10 +00:00
27 changed files with 1119 additions and 337 deletions

View File

@@ -1,4 +1,5 @@
PAYLOAD_SECRET=
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
MONGODB_HOST=localhost
MONGODB_DB=summer-dci
MONGODB_USER=

View File

@@ -1,9 +1,12 @@
{
"globals": {},
"globals": {
"Qs": "readonly"
},
"env": {
"commonjs": true,
"es2021": true,
"node": true
"node": true,
"browser": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
@@ -39,4 +42,4 @@
}
]
}
}
}

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ build/
src/media
.eslintcache
.*.swp
public/output.css
src/uploads

10
ecosystem.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
apps: [ {
name: 'summer-dci',
script: `${__dirname}/dist/server.js`,
env: {
NODE_ENV: 'production',
PAYLOAD_CONFIG_PATH: 'dist/payload.config.js'
}
} ]
};

View File

@@ -5,11 +5,13 @@
"main": "dist/server.js",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"dev:tailwind": "tailwindcss -i ./src/css/input.css -o ./public/output.css --watch",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"build:tailwind": "tailwindcss -i ./src/css/input.css -o ./public/output.css",
"build": "yarn build:tailwind && yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,pdf}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"prepare": "husky install",
@@ -20,19 +22,21 @@
"dependencies": {
"dotenv": "^16.0.1",
"express": "^4.18.1",
"payload": "^1.0.9"
"payload": "^1.0.12",
"qs": "^6.11.0"
},
"devDependencies": {
"@matt-fidd/eslint-config": "^1.3.4",
"@types/express": "^4.17.13",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.20.0",
"eslint": "^8.21.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"nodemon": "^2.0.19",
"tailwindcss": "^3.1.7",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
},
@@ -41,6 +45,9 @@
"eslint --cache --fix",
"yarn generate:types",
"git add src/payload-types.ts"
],
"*.js": [
"eslint --cache --fix"
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

67
public/index.html Normal file
View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<title>Trip Schedule</title>
<link href='output.css' rel='stylesheet'>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.11.0/qs.js"></script>
</head>
<body>
<header class='flex justify-between p-4 shadow-sm w-screen bg-white z-10'>
<div class='flex gap-6 items-center flex-wrap'>
<a href='/'>
<h1 class='text-3xl'>Trip Schedule</h1>
</a>
<nav>
<ul class='flex gap-4'>
<li><a href='/' class='hover:underline'>Home</a></li>
<li><a href='/uploads.html' class='hover:underline'>Uploads</a></li>
<li><a href='/admin' class='hover:underline'>Admin</a></li>
</ul>
</nav>
</div>
</header>
<div id='container' class='min-h-screen z-0 p-8 bg-gray-100'>
</div>
<template id='events-container'>
<div class='my-4'>
<h2 class='events-container-title text-2xl mb-4'></h2>
<div class='grid md:grid-cols-3 lg:grid-cols-4 grid-cols-1 gap-4'>
</div>
</div>
</template>
<template id='event-card'>
<div class='drop-shadow-md rounded-md text-center p-4 bg-white relative'>
<h3 class='event-name text-2xl'></h3>
<span class='event-type py-0.5 px-3 rounded my-2 inline-block'></span>
<div class='event-date text-l'>
<span class='event-start'></span>
<span class='event-time-seperator hidden'> - </span>
<span class='event-end'></span>
</div>
<div class='event-time text-l'>
<span class='event-start'></span>
<span class='event-time-seperator hidden'> - </span>
<span class='event-end'></span>
</div>
<a class='event-link absolute inset-0 block' href=''></a>
</div>
</template>
<script src='index.js' type='module'></script>
</body>
</html>

67
public/index.js Normal file
View File

@@ -0,0 +1,67 @@
const cardTemplate = document.getElementById('event-card');
const eventsContainer = document.getElementById('events-container');
const mainContainer = document.getElementById('container');
async function main() {
const query = {
sort: 'start',
limit: 50
};
const stringifiedQuery = Qs.stringify({
...query
},
{
addQueryPrefix: true
}
);
const events = await (await fetch(`/api/events${stringifiedQuery}`)).json();
let currentDate;
let currentContainer;
for (const event of events.docs) {
if (currentDate !== event.startDate) {
currentContainer && mainContainer.append(currentContainer);
const c = eventsContainer.content.cloneNode(true);
c.querySelector('.events-container-title').innerText = event.startDate;
currentContainer = c;
}
currentDate = event.startDate;
const card = cardTemplate.content.cloneNode(true);
card.querySelector('.event-name').innerText = event.name;
const typeElem = card.querySelector('.event-type');
typeElem.innerText = event.type.value.name;
typeElem.style.backgroundColor = event.type.value.backgroundColour;
card.querySelector('.event-date .event-start').innerText = event.startDate;
card.querySelector('.event-time .event-start').innerText = event.startTime;
if (event.end) {
if (event.endDate !== event.startDate) {
card.querySelector('.event-date .event-end').innerText = event.endDate;
card.querySelector('.event-date .event-time-seperator').style.display = 'inline';
}
card.querySelector('.event-time .event-end').innerText = event.endTime;
card.querySelector('.event-time .event-time-seperator').style.display = 'inline';
}
card.querySelector('a.event-link').setAttribute('href', `single.html?event=${event.id}`);
currentContainer.querySelector('.grid').append(card);
}
currentContainer && mainContainer.append(currentContainer);
}
main();

78
public/single.html Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<title>Event</title>
<link href='output.css' rel='stylesheet'>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.11.0/qs.js"></script>
</head>
<body>
<header class='flex justify-between p-4 shadow-sm w-screen bg-white z-10'>
<div class='flex gap-6 items-center flex-wrap'>
<a href='/'>
<h1 class='text-3xl'>Trip Schedule</h1>
</a>
<nav>
<ul class='flex gap-4'>
<li><a href='/' class='hover:underline'>Home</a></li>
<li><a href='/uploads.html' class='hover:underline'>Uploads</a></li>
<li><a href='/admin' class='hover:underline'>Admin</a></li>
</ul>
</nav>
</div>
</header>
<div id='container' class='min-h-screen z-0 p-8 bg-gray-100'>
<div id='event' class='drop-shadow-md rounded-md p-4 bg-white relative'>
<div class='flex gap-4 items-center flex-wrap'>
<h2 class='event-name text-3xl'></h2>
<span class='event-type py-0.5 px-3 rounded my-2 inline-block'></span>
</div>
<div class='event-time-container my-4'>
<h3 class='text-2xl'>Times</h3>
<div class='event-start text-l'>
<span>Starts at: </span>
<span class='event-datetime'></span>
</div>
<div class='event-end text-l'>
<span>Ends at: </span>
<span class='event-datetime'></span>
</div>
</div>
<div class='event-location-container my-4'>
<h3 class='text-2xl'>Location</h3>
<div class='event-location-start'>
<span>Start location: </span>
</div>
<div class='event-location-end'>
<span>End location: </span>
</div>
</div>
<div class='event-notes-container my-4'>
<h3 class='text-2xl'>Notes</h3>
<p class='event-notes'>
</p>
</div>
<div class='event-uploads-container my-4'>
<h3 class='text-2xl'>Uploads</h3>
<div class='event-uploads'>
</div>
</div>
</div>
</div>
<script src='single.js' type='module'></script>
</body>
</html>

97
public/single.js Normal file
View File

@@ -0,0 +1,97 @@
const $event = document.getElementById('event');
const searchParams = new URLSearchParams(document.location.search);
const eventId = searchParams.get('event');
const googleAPIEndpoint = 'https://www.google.com/maps/dir/?api=1';
const getDirectionLink = (location) => {
const $elem = document.createElement('a');
const encodedLocation = encodeURIComponent(location);
const dirLink = `${googleAPIEndpoint}&destination=${encodedLocation}`;
$elem.setAttribute('href', dirLink);
$elem.innerText = location;
$elem.classList.add('underline');
return $elem;
};
async function main() {
if (!eventId)
return window.location.replace('/');
const query = {
sort: 'start',
where: {
id: {
equals: searchParams.get('event')
}
}
};
const stringifiedQuery = Qs.stringify({
...query
},
{
addQueryPrefix: true
}
);
const res = await (await fetch(`/api/events${stringifiedQuery}`)).json();
const event = res.docs[0];
if (!event)
return window.location.replace('/');
document.title = `Event: ${event.name}`;
$event.querySelector('.event-name').innerText = event.name;
const typeElem = $event.querySelector('.event-type');
typeElem.innerText = event.type.value.name;
typeElem.style.backgroundColor = event.type.value.backgroundColour;
$event.querySelector('.event-start .event-datetime').innerText = event.start;
$event.querySelector('.event-end .event-datetime').innerText = event.end ? event.end : 'N/A';
if (event.startLocation) {
$event.querySelector('.event-location-start').appendChild(getDirectionLink(event.startLocation));
if (event.endLocation)
$event.querySelector('.event-location-end').appendChild(getDirectionLink(event.endLocation));
else
$event.querySelector('.event-location-end').innerText = '';
} else {
$event.querySelector('.event-location-start').innerText = 'There are no locations specified for this event';
$event.querySelector('.event-location-end').innerText = '';
}
$event.querySelector('.event-notes').innerText = event.notes ?? 'There are no notes for this event';
const uploads = $event.querySelector('.event-uploads');
if (event.uploads && event.uploads.length > 0) {
for (const { upload } of event.uploads) {
if (typeof upload === 'string') {
uploads.classList.add('text-red-600');
uploads.innerText = 'Please log in to view uploads';
break;
}
const $elem = document.createElement('a');
$elem.setAttribute('href', upload.url);
$elem.innerText = upload.filename;
$elem.classList.add('underline');
uploads.appendChild($elem);
}
} else {
uploads.innerText = 'There are no uploads for this event';
}
}
main();

19
public/site.webmanifest Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "DCI Trip Schedule",
"short_name": "Schedule",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

48
public/uploads.html Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<title>Uploads</title>
<link href='output.css' rel='stylesheet'>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.11.0/qs.js"></script>
</head>
<body>
<header class='flex justify-between p-4 shadow-sm w-screen bg-white z-10'>
<div class='flex gap-6 items-center flex-wrap'>
<a href='/'>
<h1 class='text-3xl'>Trip Schedule</h1>
</a>
<nav>
<ul class='flex gap-4'>
<li><a href='/' class='hover:underline'>Home</a></li>
<li><a href='/uploads.html' class='hover:underline'>Uploads</a></li>
<li><a href='/admin' class='hover:underline'>Admin</a></li>
</ul>
</nav>
</div>
</header>
<div id='container' class='min-h-screen z-0 p-8 bg-gray-100'>
<div id='uploads' class='drop-shadow-md rounded-md p-4 bg-white relative'>
</div>
</div>
<template id='upload-item'>
<div>
<a class='upload-link hover:underline'></a>
</div>
</template>
<script src='uploads.js' type='module'></script>
</body>
</html>

43
public/uploads.js Normal file
View File

@@ -0,0 +1,43 @@
const uploadTemplate = document.getElementById('upload-item');
const uploadsContainer = document.getElementById('uploads');
async function main() {
const query = {
sort: 'filename',
limit: 50
};
const stringifiedQuery = Qs.stringify({
...query
},
{
addQueryPrefix: true
}
);
const uploads = await (await fetch(`/api/uploads${stringifiedQuery}`)).json();
console.log(uploads);
if (uploads.errors) {
const $elem = document.createElement('span');
$elem.classList.add('text-red-600');
$elem.innerText =
uploads.errors[0].message +
'\nIf you are not logged in, please log in to view this page';
uploadsContainer.appendChild($elem);
return;
}
for (const upload of uploads.docs) {
const $upload = uploadTemplate.content.cloneNode(true);
$upload.querySelector('.upload-link').innerText = upload.filename;
$upload.querySelector('.upload-link').setAttribute('href', upload.url);
uploadsContainer.append($upload);
}
}
main();

View File

@@ -2,6 +2,32 @@ import { CollectionConfig } from 'payload/types';
const EventTypes: CollectionConfig = {
slug: 'event-types',
access: {
read: () => true,
},
hooks: {
afterRead: [
({ doc }) => {
const hashCode = (str) => {
let hash = 0;
for (let i = 0; i < str.length; i++)
hash = str.charCodeAt(i) + ((hash << 5) - hash);
return hash;
};
const pickColour = (str) => {
return `hsl(${hashCode(str) % 360}, 100%, 65%)`;
};
doc.backgroundColour = pickColour(doc.name);
return doc;
}
]
},
admin: {
useAsTitle: 'name'
},

View File

@@ -5,7 +5,32 @@ const Events: CollectionConfig = {
admin: {
useAsTitle: 'name'
},
access: {
read: () => true,
},
timestamps: false,
hooks: {
afterRead: [
({ doc }) => {
const [ startDate, startTime ] = doc?.start?.split(' ') ?? [ null, null ];
const [ endDate, endTime ] = doc?.end?.split(' ') ?? [ null, null ];
Object.assign(doc, {
startTime,
startDate
});
if (endTime && endDate) {
Object.assign(doc, {
endTime,
endDate
});
}
return doc;
},
]
},
fields: [
{
name: 'name',
@@ -26,45 +51,106 @@ const Events: CollectionConfig = {
type: 'row',
fields: [
{
name: 'startTime',
label: 'Start Time',
type: 'date',
name: 'start',
label: 'Start',
type: 'text',
required: true,
validate: (val) => {
const regex = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/;
if (!regex.test(val))
return 'Must be in the form dd/mm/yyyy HH:mm';
return true;
},
admin: {
width: '50%',
date: {
displayFormat: 'MMM d, yyy HH:mm',
timeFormat: 'HH:mm',
timeIntervals: 15
}
description: 'Please fill out in the form dd/mm/yyyy HH:mm'
}
},
{
name: 'endTime',
label: 'End Time',
type: 'date',
name: 'end',
label: 'End',
type: 'text',
validate: (val, { siblingData }) => {
if (!val)
return true;
const end = new Date(val).getTime();
const start = new Date(siblingData.startTime).getTime();
const regex = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/;
if (end >= start)
return true;
if (!regex.test(val))
return 'Must be in the form dd/mm/yyyy HH:mm';
return 'End date must be greater than or equal to the start date';
const end = val;
const start = siblingData.start;
const [ startDateObject, endDateObject ] = [ start, end ].map(v => {
const d = new Date();
const [ date, time ] = v.split(' ');
const [ day, month, year ] = date.split('/');
const [ hours, minutes ] = time.split(':');
d.setFullYear(year, month - 1, day);
d.setHours(hours, minutes, 0, 0);
return d;
});
if (endDateObject < startDateObject)
return 'End date must be greater than or equal to the start date';
return true;
},
admin: {
width: '50%',
date: {
displayFormat: 'MMM d, yyy HH:mm',
timeFormat: 'HH:mm',
timeIntervals: 15
}
description: 'Please fill out in the form dd/mm/yyyy HH:mm'
}
}
]
},
{
type: 'row',
fields: [
{
name: 'startLocation',
label: 'Start Location',
type: 'text',
admin: {
width: '50%',
}
},
{
name: 'endLocation',
label: 'End Location',
type: 'text',
admin: {
width: '50%',
}
},
]
},
{
name: 'notes',
label: 'Notes',
type: 'textarea'
},
{
name: 'uploads',
label: 'Uploads',
type: 'array',
labels: {
singular: 'file',
plural: 'files'
},
fields: [
{
name: 'upload',
type: 'upload',
relationTo: 'uploads',
required: true
}
]
}
]
};

View File

@@ -0,0 +1,12 @@
import { CollectionConfig } from 'payload/types';
const Uploads: CollectionConfig = {
slug: 'uploads',
upload: {
staticURL: '/uploads',
staticDir: 'uploads'
},
fields: []
};
export default Uploads;

5
src/css/input.css Normal file
View File

@@ -0,0 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -39,6 +39,26 @@ export interface Event {
value: string | EventType;
relationTo: 'event-types';
};
startTime: string;
endTime?: string;
start: string;
end?: string;
startLocation?: string;
endLocation?: string;
notes?: string;
uploads: {
upload: string | Upload;
id?: string;
}[];
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "uploads".
*/
export interface Upload {
id: string;
url?: string;
filename?: string;
mimeType?: string;
filesize?: number;
createdAt: string;
updatedAt: string;
}

View File

@@ -1,19 +1,26 @@
import { buildConfig } from 'payload/config';
import dotenv from 'dotenv';
import path from 'path';
import EventTypes from './collections/EventTypes';
import Events from './collections/Events';
import Uploads from './collections/Uploads';
import Users from './collections/Users';
dotenv.config({
path: path.resolve(__dirname, '../.env'),
});
export default buildConfig({
serverURL: 'https://summer.mattfidd.rocks',
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
admin: {
user: Users.slug
},
collections: [
Users,
EventTypes,
Events
Events,
Uploads,
],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts')

View File

@@ -1,11 +1,14 @@
import * as dotenv from 'dotenv';
import express from 'express';
import path from 'path';
import payload from 'payload';
import findEventsOnDay from './helpers/findEventsOnDay';
// import findEventsOnDay from './helpers/findEventsOnDay';
dotenv.config();
dotenv.config({
path: path.join(__dirname, '../.env')
});
const app = express();
@@ -30,18 +33,6 @@ payload.init({
}
});
app.get('/', async (req, res) => {
const docs = await payload.find({
collection: 'events',
sort: 'startTime',
pagination: false
});
res.json(docs);
});
app.get('/day/:day', async (req, res) => {
res.json(await findEventsOnDay(payload, req.params.day));
});
app.use(express.static(path.join(__dirname, '../public')));
app.listen(process.env.LISTEN_PORT);

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
content: [ './public/**/*.{html,js}' ],
theme: {
extend: {
fontFamily: {
sans: [ 'Lato', 'sans-serif' ]
}
},
},
plugins: [],
};

756
yarn.lock

File diff suppressed because it is too large Load Diff