Compare commits
24 Commits
date-field
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 243db422b1 | |||
| 217f86e5af | |||
| 91d16ced01 | |||
| 4f20c073cc | |||
| 1065055cdb | |||
| 7a6cc510a2 | |||
| 9c30d14975 | |||
| 4418ec2b82 | |||
| e260756ce9 | |||
| af4f6f6f16 | |||
| d342799127 | |||
| b323e3b70a | |||
| 4aae9867ef | |||
| 262689e140 | |||
| 37e77d3405 | |||
| 5cdf43c845 | |||
| 91ae70b190 | |||
| b504d3235e | |||
| 1b52f6bea3 | |||
| 909cd08275 | |||
| fa6e949805 | |||
| 4b4c1d5a1d | |||
| 88ead4ff03 | |||
| 6b6eacf45d |
@@ -1,4 +1,5 @@
|
||||
PAYLOAD_SECRET=
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
MONGODB_HOST=localhost
|
||||
MONGODB_DB=summer-dci
|
||||
MONGODB_USER=
|
||||
|
||||
@@ -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
2
.gitignore
vendored
@@ -5,3 +5,5 @@ build/
|
||||
src/media
|
||||
.eslintcache
|
||||
.*.swp
|
||||
public/output.css
|
||||
src/uploads
|
||||
|
||||
10
ecosystem.config.js
Normal file
10
ecosystem.config.js
Normal 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'
|
||||
}
|
||||
} ]
|
||||
};
|
||||
21
package.json
21
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/android-chrome-192x192.png
Normal file
BIN
public/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/android-chrome-512x512.png
Normal file
BIN
public/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
public/apple-touch-icon.png
Normal file
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
BIN
public/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 714 B |
BIN
public/favicon-32x32.png
Normal file
BIN
public/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
67
public/index.html
Normal file
67
public/index.html
Normal 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
67
public/index.js
Normal 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
78
public/single.html
Normal 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
97
public/single.js
Normal 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
19
public/site.webmanifest
Normal 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
48
public/uploads.html
Normal 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
43
public/uploads.js
Normal 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();
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -5,13 +5,30 @@ const Events: CollectionConfig = {
|
||||
admin: {
|
||||
useAsTitle: 'name'
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
timestamps: false,
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ doc }) => {
|
||||
delete doc.UTCOffset;
|
||||
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: [
|
||||
@@ -34,77 +51,107 @@ 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
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [ ({ value, siblingData }) => {
|
||||
const d = new Date(value);
|
||||
|
||||
const hourOffset = parseInt(siblingData.UTCOffset) / -60;
|
||||
|
||||
d.setHours(d.getHours() + hourOffset);
|
||||
|
||||
return d;
|
||||
} ]
|
||||
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
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [ ({ value, siblingData }) => {
|
||||
const d = new Date(value);
|
||||
|
||||
const hourOffset = parseInt(siblingData.UTCOffset) / -60;
|
||||
|
||||
d.setHours(d.getHours() + hourOffset);
|
||||
|
||||
return d;
|
||||
} ]
|
||||
description: 'Please fill out in the form dd/mm/yyyy HH:mm'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'UTCOffset',
|
||||
type: 'text',
|
||||
defaultValue: () => new Date().getTimezoneOffset(),
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
readOnly: true,
|
||||
},
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
12
src/collections/Uploads.ts
Normal file
12
src/collections/Uploads.ts
Normal 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
5
src/css/input.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -39,7 +39,26 @@ export interface Event {
|
||||
value: string | EventType;
|
||||
relationTo: 'event-types';
|
||||
};
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
UTCOffset?: 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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
content: [ './public/**/*.{html,js}' ],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [ 'Lato', 'sans-serif' ]
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user