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_SECRET=
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
MONGODB_HOST=localhost
|
MONGODB_HOST=localhost
|
||||||
MONGODB_DB=summer-dci
|
MONGODB_DB=summer-dci
|
||||||
MONGODB_USER=
|
MONGODB_USER=
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{
|
{
|
||||||
"globals": {},
|
"globals": {
|
||||||
|
"Qs": "readonly"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"commonjs": true,
|
"commonjs": true,
|
||||||
"es2021": true,
|
"es2021": true,
|
||||||
"node": true
|
"node": true,
|
||||||
|
"browser": true
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@ build/
|
|||||||
src/media
|
src/media
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.*.swp
|
.*.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'
|
||||||
|
}
|
||||||
|
} ]
|
||||||
|
};
|
||||||
19
package.json
19
package.json
@@ -5,11 +5,13 @@
|
|||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
"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:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||||
"build:server": "tsc",
|
"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",
|
"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: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",
|
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
@@ -20,19 +22,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"payload": "^1.0.9"
|
"payload": "^1.0.12",
|
||||||
|
"qs": "^6.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@matt-fidd/eslint-config": "^1.3.4",
|
"@matt-fidd/eslint-config": "^1.3.4",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||||
"@typescript-eslint/parser": "^5.30.7",
|
"@typescript-eslint/parser": "^5.32.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.20.0",
|
"eslint": "^8.21.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
|
"tailwindcss": "^3.1.7",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.7.4"
|
||||||
},
|
},
|
||||||
@@ -41,6 +45,9 @@
|
|||||||
"eslint --cache --fix",
|
"eslint --cache --fix",
|
||||||
"yarn generate:types",
|
"yarn generate:types",
|
||||||
"git add src/payload-types.ts"
|
"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 = {
|
const EventTypes: CollectionConfig = {
|
||||||
slug: 'event-types',
|
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: {
|
admin: {
|
||||||
useAsTitle: 'name'
|
useAsTitle: 'name'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,13 +5,30 @@ const Events: CollectionConfig = {
|
|||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'name'
|
useAsTitle: 'name'
|
||||||
},
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
hooks: {
|
hooks: {
|
||||||
afterRead: [
|
afterRead: [
|
||||||
({ doc }) => {
|
({ 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;
|
return doc;
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
@@ -34,77 +51,107 @@ const Events: CollectionConfig = {
|
|||||||
type: 'row',
|
type: 'row',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'startTime',
|
name: 'start',
|
||||||
label: 'Start Time',
|
label: 'Start',
|
||||||
type: 'date',
|
type: 'text',
|
||||||
required: true,
|
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: {
|
admin: {
|
||||||
width: '50%',
|
width: '50%',
|
||||||
date: {
|
description: 'Please fill out in the form dd/mm/yyyy HH:mm'
|
||||||
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;
|
|
||||||
} ]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'endTime',
|
name: 'end',
|
||||||
label: 'End Time',
|
label: 'End',
|
||||||
type: 'date',
|
type: 'text',
|
||||||
validate: (val, { siblingData }) => {
|
validate: (val, { siblingData }) => {
|
||||||
if (!val)
|
if (!val)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
const end = new Date(val).getTime();
|
const regex = /^\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}$/;
|
||||||
const start = new Date(siblingData.startTime).getTime();
|
|
||||||
|
|
||||||
if (end >= start)
|
if (!regex.test(val))
|
||||||
return true;
|
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: {
|
admin: {
|
||||||
width: '50%',
|
width: '50%',
|
||||||
date: {
|
description: 'Please fill out in the form dd/mm/yyyy HH:mm'
|
||||||
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;
|
|
||||||
} ]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'UTCOffset',
|
type: 'row',
|
||||||
type: 'text',
|
fields: [
|
||||||
defaultValue: () => new Date().getTimezoneOffset(),
|
{
|
||||||
admin: {
|
name: 'startLocation',
|
||||||
position: 'sidebar',
|
label: 'Start Location',
|
||||||
readOnly: true,
|
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;
|
value: string | EventType;
|
||||||
relationTo: 'event-types';
|
relationTo: 'event-types';
|
||||||
};
|
};
|
||||||
startTime: string;
|
start: string;
|
||||||
endTime?: string;
|
end?: string;
|
||||||
UTCOffset?: 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 { buildConfig } from 'payload/config';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import EventTypes from './collections/EventTypes';
|
import EventTypes from './collections/EventTypes';
|
||||||
import Events from './collections/Events';
|
import Events from './collections/Events';
|
||||||
|
import Uploads from './collections/Uploads';
|
||||||
import Users from './collections/Users';
|
import Users from './collections/Users';
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(__dirname, '../.env'),
|
||||||
|
});
|
||||||
|
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
serverURL: 'https://summer.mattfidd.rocks',
|
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||||
admin: {
|
admin: {
|
||||||
user: Users.slug
|
user: Users.slug
|
||||||
},
|
},
|
||||||
collections: [
|
collections: [
|
||||||
Users,
|
Users,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
Events
|
Events,
|
||||||
|
Uploads,
|
||||||
],
|
],
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(__dirname, 'payload-types.ts')
|
outputFile: path.resolve(__dirname, 'payload-types.ts')
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import path from 'path';
|
||||||
import payload from 'payload';
|
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();
|
const app = express();
|
||||||
|
|
||||||
@@ -30,18 +33,6 @@ payload.init({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
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.listen(process.env.LISTEN_PORT);
|
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