Skip to content

Commit

Permalink
feat: add foreign-collections-list page
Browse files Browse the repository at this point in the history
  • Loading branch information
Eejit43 committed Mar 6, 2024
1 parent 4ec6001 commit 28fd41c
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/public/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const preParsedPages: PreParsedPages = {
'coins-info': { title: 'Coins Info', icon: 'coin', description: 'Information about all coins that have been produced for circulation in the United States' },
'coins-list': { title: 'Coins List', icon: 'coins', description: 'A list of coins I have and need' },
'euro-coins': { title: 'Euro Coins', icon: 'euro-sign', description: 'A list of all euro coin designs', additionalData: { euroCoins } },
'foreign-collections-list': { title: 'Foreign Collection List', icon: 'earth-americas', description: 'A list of countries I have coins, stamps, and banknotes from' },
'minecraft-codes': {
title: 'Minecraft Formatting Codes',
icon: 'gamepad-modern',
Expand Down
26 changes: 15 additions & 11 deletions src/public/scripts/pages/info/coins-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ loginButton.addEventListener('click', async () => {
showAlert('Logged in!', 'success');
showResult(loginButton, 'success', false);

loadCoinsList();
await loadCoinsList();

exportDataButton.disabled = false;
} else {
Expand Down Expand Up @@ -736,11 +736,13 @@ function formatMintage(mintage: number) {
* @param data The data to update.
*/
async function updateCoinData(denominationId: string, designId: string, coinId: string, data: PartialNullable<Coin>) {
const result = (await fetch('/coins-list-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ denominationId, designId, coinId, data, password: passwordInput.dataset.input }),
})) as { error?: string };
const result = (await (
await fetch('/coins-list-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ denominationId, designId, coinId, data, password: passwordInput.dataset.input }),
})
).json()) as { error?: string };

if (result.error) showAlert(result.error, 'error');
else showAlert('Coin data updated successfully!', 'success');
Expand All @@ -754,11 +756,13 @@ async function updateCoinData(denominationId: string, designId: string, coinId:
* @param coinId The id of the coin to add.
*/
async function addCoin(denominationId: string, designId: string, coinYear: string, coinId: string) {
const result = (await fetch('/coins-list-add-coin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ denominationId, designId, coinYear, coinId, password: passwordInput.dataset.input }),
})) as { error?: string };
const result = (await (
await fetch('/coins-list-add-coin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ denominationId, designId, coinYear, coinId, password: passwordInput.dataset.input }),
})
).json()) as { error?: string };

if (result.error) showAlert(result.error, 'error');
else showAlert('Successfully added a new coin row!', 'success');
Expand Down
188 changes: 188 additions & 0 deletions src/public/scripts/pages/info/foreign-collections-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { ForeignCollectionsList } from '../../../../route-handlers/foreign-collections-list.js';
import { showAlert, showResult } from '../../functions.js';

const passwordInput = document.querySelector('#login-password') as HTMLInputElement;
const loginButton = document.querySelector('#login-button') as HTMLButtonElement;
const collectionsListMessage = document.querySelector('#collections-list-message') as HTMLDivElement;
const collectionsList = document.querySelector('#collections-list') as HTMLDivElement;
const newRowMessage = document.querySelector('#new-row-message') as HTMLTableRowElement;
const exportDataButton = document.querySelector('#export-data') as HTMLButtonElement;

for (const type of ['input', 'paste'])
passwordInput.addEventListener(type, () => {
passwordInput.value = passwordInput.value.replaceAll(/\D/g, '');

if (passwordInput.value.length > 4) passwordInput.value = passwordInput.value.slice(0, 4);
});

passwordInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && passwordInput.value.length > 0) loginButton.click();
});

loginButton.addEventListener('click', async () => {
const { success } = (await (await fetch(`/coins-login?password=${passwordInput.value}`)).json()) as { success: boolean };

if (success) {
passwordInput.dataset.input = passwordInput.value;
passwordInput.value = '';
passwordInput.disabled = true;
loginButton.disabled = true;
showAlert('Logged in!', 'success');
showResult(loginButton, 'success', false);

await loadCollectionList();

exportDataButton.disabled = false;
} else {
showAlert('Incorrect password!', 'error');
showResult(loginButton, 'error');
loginButton.disabled = true;
setTimeout(() => (loginButton.disabled = false), 1000);
}
});

let collectionData: ForeignCollectionsList;

/**
* Load the coins list.
*/
async function loadCollectionList() {
collectionData = (await (await fetch(`/foreign-collections-list?password=${passwordInput.dataset.input!}`)).json()) as ForeignCollectionsList;

collectionsListMessage.style.display = 'none';
collectionsList.style.display = 'block';

reloadTableData();

newRowMessage.addEventListener('click', async () => {
let id: string;
do id = Math.floor(Math.random() * 9_000_000_000 + 1_000_000_000).toString();
while (collectionData.some((country) => country.id === id));

const result = (await (
await fetch('/foreign-collections-list-add-country', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Unknown Country', data: { coins: false, banknotes: false, stamps: false }, password: passwordInput.dataset.input }),
})
).json()) as { error?: string; data: ForeignCollectionsList };

if (result.error) showAlert(result.error, 'error');
else {
showAlert('Successfully added a new country row!', 'success');

collectionData = result.data;

reloadTableData();
}
});
}

/**
* Reloads the table's data from the stored variable.
*/
function reloadTableData() {
const tableBody = collectionsList.querySelector('tbody') as HTMLTableSectionElement;

while (tableBody.children.length > 1) tableBody.children[0].remove();

for (const country of collectionData) {
const row = document.createElement('tr');

const nameCell = document.createElement('td');
nameCell.textContent = country.name;
nameCell.contentEditable = 'true';
nameCell.addEventListener('blur', async () => {
const result = (await (
await fetch('/foreign-collections-list-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: country.id, name: nameCell.textContent, data: country.data, password: passwordInput.dataset.input }),
})
).json()) as { error?: string; data: ForeignCollectionsList };

if (result.error) showAlert(result.error, 'error');
else {
showAlert('Successfully edited the country name!', 'success');

collectionData = result.data;

reloadTableData();
}
});
row.append(nameCell);

for (const type of ['coins', 'banknotes', 'stamps'] as const) {
const obtained = country.data[type];

const cell = document.createElement('td');
row.append(cell);

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = obtained;
checkbox.addEventListener('change', async () => {
let sortedData = { ...country.data, [type]: checkbox.checked };
sortedData = { coins: sortedData.coins, banknotes: sortedData.banknotes, stamps: sortedData.stamps };

const result = (await (
await fetch('/foreign-collections-list-edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: country.id, name: country.name, data: sortedData, password: passwordInput.dataset.input }),
})
).json()) as { error?: string; data: ForeignCollectionsList };

if (result.error) showAlert(result.error, 'error');
else {
showAlert(`Successfully updated the country's "${type}" obtained status!`, 'success');

collectionData = result.data;

reloadTableData();
}
});
cell.append(checkbox);
}

newRowMessage.before(row);
}
}

const parameters = new URLSearchParams(window.location.search);
const password = parameters.get('password');

if (password) {
const { success } = (await (await fetch(`/coins-login?password=${password}`)).json()) as { success: boolean };

if (success) {
passwordInput.dataset.input = password;
passwordInput.disabled = true;
loginButton.disabled = true;
showAlert('Logged in!', 'success');
showResult(loginButton, 'success', false);

await loadCollectionList();

exportDataButton.disabled = false;
} else {
showAlert('Incorrect password!', 'error');
showResult(loginButton, 'error');
}
}

// Add functionality to data exporter
exportDataButton.addEventListener('click', async () => {
const coinsData = (await (await fetch(`/foreign-collections-list?password=${passwordInput.dataset.input!}`)).json()) as ForeignCollectionsList & { error?: string };

if (coinsData.error) return showAlert(coinsData.error, 'error');

const file = new Blob([JSON.stringify(coinsData)], { type: 'application/json' });
const anchor = document.createElement('a');
const url = URL.createObjectURL(file);
anchor.href = url;
anchor.download = `foreign-collection-list data (${new Date().toLocaleString()}).json`;
anchor.click();

setTimeout(() => URL.revokeObjectURL(url), 0);
});
38 changes: 38 additions & 0 deletions src/public/styles/pages/info/foreign-collections-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#login-container {
margin-top: 1em;
}

#login-password {
width: calc(4ch + 12px);
}

#collection-list table {
margin: 20px auto;
width: 50%;

& input[type="checkbox"] {
transform: scale(1.3);
}
}

#collection-list th {
text-align: center;
}

#new-row-message {
transition: all 0.2s ease;
cursor: pointer;

&:hover {
background-color: #3e434b;
}

& i.fa-plus {
padding-right: 6px;
}
}

button#export-data {
margin-top: 1em;
padding: 5px;
}
85 changes: 85 additions & 0 deletions src/route-handlers/foreign-collections-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { FastifyInstance, FastifyRequest } from 'fastify';
import { Schema, model } from 'mongoose';

interface ForeignCollectionCountry {
id: string;
name: string;
data: { coins: boolean; banknotes: boolean; stamps: boolean };
}

export type ForeignCollectionsList = ForeignCollectionCountry[];

type DatabaseForeignCollectionsList = { data: ForeignCollectionsList } & { _id?: number; __v?: number }; // eslint-disable-line @typescript-eslint/naming-convention

const foreignCollectionsList = model('foreign-collections-list', new Schema({ data: Array }, {}));

/**
* Sets up all foreign collection related routes.
* @param fastify The Fastify instance.
*/
export default function (fastify: FastifyInstance) {
fastify.get('/foreign-collections-list', async (request: FastifyRequest<{ Querystring: { password: string } }>, reply) => {
if (request.query.password !== process.env.COINS_PASSWORD) return reply.send(JSON.stringify({ error: 'Invalid password!' }, null, 2));

const foundList = (await foreignCollectionsList.findOne({}).lean()) as DatabaseForeignCollectionsList;

reply.send(JSON.stringify(foundList.data, null, 2));
});

fastify.post('/foreign-collections-list-edit', async (request: FastifyRequest<{ Body: ForeignCollectionCountry & { password: string } }>, reply) => {
const { name, data, password } = request.body;

if (password !== process.env.COINS_PASSWORD) return reply.send(JSON.stringify({ error: 'Invalid password!' }, null, 2));

const foundList = (await foreignCollectionsList.findOne({}).lean()) as DatabaseForeignCollectionsList;

delete foundList._id;
delete foundList.__v;

const foundCountry = foundList.data.find((country) => country.id === request.body.id);
if (!foundCountry) return reply.send(JSON.stringify({ error: 'Country not found!' }, null, 2));

foundCountry.name = name;
foundCountry.data = data;

const sortedData = foundList.data.sort((a, b) => a.name.localeCompare(b.name));

await foreignCollectionsList.replaceOne({}, { data: sortedData });

reply.send(JSON.stringify({ success: true, data: sortedData }, null, 2));
});

fastify.post('/foreign-collections-list-add-country', async (request: FastifyRequest<{ Body: ForeignCollectionCountry & { password: string } }>, reply) => {
const { name, data, password } = request.body;

if (password !== process.env.COINS_PASSWORD) return reply.send(JSON.stringify({ error: 'Invalid password!' }, null, 2));

const foundList = (await foreignCollectionsList.findOne({}).lean()) as DatabaseForeignCollectionsList;

delete foundList._id;
delete foundList.__v;

const id = generateUniqueCountryId(foundList.data);

foundList.data.push({ id, name, data });

const sortedData = foundList.data.sort((a, b) => a.name.localeCompare(b.name));

await foreignCollectionsList.replaceOne({}, { data: sortedData });

reply.send(JSON.stringify({ success: true, data: sortedData }, null, 2));
});
}

/**
* Generates a unique coin ID.
* @param list The list of collections to check against.
*/
function generateUniqueCountryId(list: ForeignCollectionsList) {
let id: string;

do id = Math.floor(Math.random() * 9_000_000_000 + 1_000_000_000).toString();
while (list.some((country) => country.id === id));

return id;
}
2 changes: 2 additions & 0 deletions src/route-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import setupCalendarRoutes from './calendar.js';
import setupCoinsInfoRoute from './coins-info.js';
import setupCoinsListRoutes from './coins-list.js';
import setupCorsAnywhereRoute from './cors-anywhere.js';
import setupForeignCollectionsList from './foreign-collections-list.js';
import setupIpInfoRoute from './ip-info.js';
import setupTidesInfoRoute from './tides-info.js';
import setupTwemojiRoute from './twemoji.js';
Expand All @@ -21,6 +22,7 @@ export default function setupRoutes(fastify: FastifyInstance) {
setupCoinsInfoRoute(fastify);
setupCoinsListRoutes(fastify);
setupCorsAnywhereRoute(fastify);
setupForeignCollectionsList(fastify);
setupIpInfoRoute(fastify);
setupTidesInfoRoute(fastify);
setupTwemojiRoute(fastify);
Expand Down
Loading

0 comments on commit 28fd41c

Please sign in to comment.