Add untile eslint and prettier config
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:@tanstack/eslint-plugin-query/recommended",
|
||||
'plugin:@next/next/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
'@untile/eslint-config-typescript-react'
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@tanstack/query"],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@tanstack/query'],
|
||||
rules: {
|
||||
'id-length': 'off',
|
||||
'id-match': ['error', '^_$|^_?[a-zA-Z][_a-zA-Z0-9]*$|^[A-Z][_A-Z0-9]+[A-Z0-9]$'],
|
||||
'no-underscore-dangle': 'off',
|
||||
'react/no-unknown-property': 'off',
|
||||
'react/react-in-jsx-scope': 'off'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import process from "process";
|
||||
import { spawn } from "child_process";
|
||||
import process from 'process';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
export const asyncSpawn = (command: string, args: string[]) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.stdout?.on("data", process.stdout.write);
|
||||
child.stderr?.on("data", process.stderr.write);
|
||||
child.on("close", (code) => {
|
||||
child.on('error', reject);
|
||||
child.stdout?.on('data', process.stdout.write);
|
||||
child.stderr?.on('data', process.stderr.write);
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
export const db = new Database("./prisma/main.db");
|
||||
export const db = new Database('./prisma/main.db');
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import path from "path";
|
||||
import Vibrant from "node-vibrant";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { asyncSpawn } from "./async-spawn";
|
||||
import puppeteer, { Page } from "puppeteer";
|
||||
import { writeFile } from "fs/promises";
|
||||
import path from 'path';
|
||||
import Vibrant from 'node-vibrant';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { asyncSpawn } from './async-spawn';
|
||||
import puppeteer, { Page } from 'puppeteer';
|
||||
import { writeFile } from 'fs/promises';
|
||||
|
||||
const baseDir = "./public";
|
||||
const baseDir = './public';
|
||||
|
||||
export const downloadFile = async (page: Page, url: string) => {
|
||||
const filepath = url.split("catalogue")[1];
|
||||
const folder = path.join(baseDir, filepath.split("/").slice(0, -1).join("/"));
|
||||
const filepath = url.split('catalogue')[1];
|
||||
const folder = path.join(baseDir, filepath.split('/').slice(0, -1).join('/'));
|
||||
console.log(url);
|
||||
await asyncSpawn("mkdir", ["-p", folder]);
|
||||
await asyncSpawn('mkdir', ['-p', folder]);
|
||||
const destination = path.join(baseDir, filepath);
|
||||
|
||||
const pageRes = await page.goto(url, { waitUntil: "load" });
|
||||
const pageRes = await page.goto(url, { waitUntil: 'load' });
|
||||
|
||||
if (pageRes) {
|
||||
await writeFile(destination, await pageRes.buffer());
|
||||
@@ -31,22 +31,22 @@ export const fetchTextures = async (type: NumistaType) => {
|
||||
|
||||
if (type?.obverse?.picture) {
|
||||
const destination = await downloadFile(page, type.obverse.picture);
|
||||
type.obverse.picture = destination.split("public")[1];
|
||||
type.obverse.picture = destination.split('public')[1];
|
||||
const palette = await Vibrant.from(destination).getPalette();
|
||||
|
||||
if (palette.Vibrant) {
|
||||
type.color = "rgb(" + palette.Vibrant.rgb.join(",") + ")";
|
||||
type.color = 'rgb(' + palette.Vibrant.rgb.join(',') + ')';
|
||||
}
|
||||
}
|
||||
|
||||
if (type?.reverse?.picture) {
|
||||
const destination = await downloadFile(page, type.reverse.picture);
|
||||
type.reverse.picture = destination.split("public")[1];
|
||||
type.reverse.picture = destination.split('public')[1];
|
||||
}
|
||||
|
||||
if (type?.edge?.picture) {
|
||||
const destination = await downloadFile(page, type.edge.picture);
|
||||
type.edge.picture = destination.split("public")[1];
|
||||
type.edge.picture = destination.split('public')[1];
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { asyncSpawn } from "./async-spawn";
|
||||
import { fetchTextures } from "./fetch-textures";
|
||||
import { db } from "./db";
|
||||
import { handleInsertType, handleUpdateTypeCount } from "./insert";
|
||||
import { readFile, rm } from "fs/promises";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { asyncSpawn } from './async-spawn';
|
||||
import { fetchTextures } from './fetch-textures';
|
||||
import { db } from './db';
|
||||
import { handleInsertType, handleUpdateTypeCount } from './insert';
|
||||
import { readFile, rm } from 'fs/promises';
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [id, count] = process.argv.slice(2, 4).map((value) => Number(value));
|
||||
let [id, count] = process.argv.slice(2, 4).map(value => Number(value));
|
||||
|
||||
if (!id) {
|
||||
throw new Error("Invalid id " + id);
|
||||
throw new Error('Invalid id ' + id);
|
||||
}
|
||||
|
||||
if (!count) {
|
||||
count = 1;
|
||||
}
|
||||
|
||||
const query = db.query("SELECT * FROM NumistaType WHERE id = " + id + ";");
|
||||
const query = db.query('SELECT * FROM NumistaType WHERE id = ' + id + ';');
|
||||
let type = query.get() as NumistaType | null;
|
||||
|
||||
if (type) {
|
||||
@@ -24,15 +24,15 @@ if (type) {
|
||||
type.count = (type.count ?? 1) + count;
|
||||
handleUpdateTypeCount(type);
|
||||
} else {
|
||||
await asyncSpawn("sh", [
|
||||
"-c",
|
||||
`curl https://api.numista.com/api/v3/types/${id} -H Numista-API-Key:egr10utzWmYztFrZhJSRGF7Tv64RA3b2S4xdz4di | jq '.count='${count} > tmp-${id}.json`,
|
||||
await asyncSpawn('sh', [
|
||||
'-c',
|
||||
`curl https://api.numista.com/api/v3/types/${id} -H Numista-API-Key:egr10utzWmYztFrZhJSRGF7Tv64RA3b2S4xdz4di | jq '.count='${count} > tmp-${id}.json`
|
||||
]);
|
||||
|
||||
type = JSON.parse(await readFile(`tmp-${id}.json`, { encoding: "utf-8" }));
|
||||
type = JSON.parse(await readFile(`tmp-${id}.json`, { encoding: 'utf-8' }));
|
||||
|
||||
if (!type) {
|
||||
throw new Error("Invalid response for id " + id);
|
||||
throw new Error('Invalid response for id ' + id);
|
||||
}
|
||||
|
||||
console.log(type.title);
|
||||
@@ -42,7 +42,7 @@ if (type) {
|
||||
type = await fetchTextures(type);
|
||||
|
||||
if (!type) {
|
||||
throw new Error("Error processing textures for id " + id);
|
||||
throw new Error('Error processing textures for id ' + id);
|
||||
}
|
||||
|
||||
handleInsertType(type);
|
||||
|
||||
122
bin/insert.ts
122
bin/insert.ts
@@ -1,28 +1,20 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { db } from "./db";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { db } from './db';
|
||||
|
||||
const omit = (data: object, keys: string[] = []) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(data).filter(
|
||||
([key, value]) =>
|
||||
!keys.includes(key) &&
|
||||
(typeof value === "number" || typeof value === "boolean" || !!value),
|
||||
),
|
||||
([key, value]) => !keys.includes(key) && (typeof value === 'number' || typeof value === 'boolean' || !!value)
|
||||
)
|
||||
);
|
||||
|
||||
const exists = (table: string, primaryKey: string, value: unknown) => {
|
||||
const stmt = `SELECT * FROM ${table} WHERE ${primaryKey}=${
|
||||
typeof value === "string" ? `'${value}'` : value
|
||||
}`;
|
||||
const stmt = `SELECT * FROM ${table} WHERE ${primaryKey}=${typeof value === 'string' ? `'${value}'` : value}`;
|
||||
|
||||
return !!db.prepare(stmt).get();
|
||||
};
|
||||
|
||||
const inserter = (
|
||||
table: string,
|
||||
data: Record<string, unknown>,
|
||||
primaryKey?: string,
|
||||
) => {
|
||||
const inserter = (table: string, data: Record<string, unknown>, primaryKey?: string) => {
|
||||
if (primaryKey && exists(table, primaryKey, data[primaryKey])) {
|
||||
console.error(`${table} ${data[primaryKey]} already exists`);
|
||||
return;
|
||||
@@ -30,19 +22,17 @@ const inserter = (
|
||||
|
||||
const entries = Object.entries(data);
|
||||
|
||||
const stmt = `INSERT INTO ${table} (${entries
|
||||
.map(([key]) => key)
|
||||
.join(", ")}) VALUES (${entries
|
||||
const stmt = `INSERT INTO ${table} (${entries.map(([key]) => key).join(', ')}) VALUES (${entries
|
||||
.map(([, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return `'${value.join(", ").replaceAll("'", "''")}'`;
|
||||
return `'${value.join(', ').replaceAll("'", "''")}'`;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (typeof value === 'string') {
|
||||
return `'${value.replaceAll("'", "''")}'`;
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.join(", ")});`;
|
||||
.join(', ')});`;
|
||||
|
||||
console.log(stmt);
|
||||
db.exec(stmt);
|
||||
@@ -50,54 +40,50 @@ const inserter = (
|
||||
|
||||
export const handleInsertType = (type: NumistaType) => {
|
||||
if (type.issuer) {
|
||||
inserter("Issuer", omit(type.issuer), "code");
|
||||
inserter('Issuer', omit(type.issuer), 'code');
|
||||
}
|
||||
if (type.value) {
|
||||
if (type.value.currency) {
|
||||
inserter("Currency", omit(type.value.currency), "id");
|
||||
inserter('Currency', omit(type.value.currency), 'id');
|
||||
}
|
||||
|
||||
inserter(
|
||||
"Value",
|
||||
'Value',
|
||||
omit(
|
||||
{
|
||||
...type.value,
|
||||
currency_id: type.value.currency?.id,
|
||||
id: type.id,
|
||||
id: type.id
|
||||
},
|
||||
["currency"],
|
||||
['currency']
|
||||
),
|
||||
"id",
|
||||
'id'
|
||||
);
|
||||
}
|
||||
if (type.demonetization) {
|
||||
inserter(
|
||||
"Demonetization",
|
||||
omit({ ...type.demonetization, id: type.id }),
|
||||
"id",
|
||||
);
|
||||
inserter('Demonetization', omit({ ...type.demonetization, id: type.id }), 'id');
|
||||
}
|
||||
if (type.composition) {
|
||||
inserter("Composition", omit({ ...type.composition, id: type.id }), "id");
|
||||
inserter('Composition', omit({ ...type.composition, id: type.id }), 'id');
|
||||
}
|
||||
if (type.technique) {
|
||||
inserter("Technique", omit({ ...type.technique, id: type.id }), "id");
|
||||
inserter('Technique', omit({ ...type.technique, id: type.id }), 'id');
|
||||
}
|
||||
if (type.obverse) {
|
||||
inserter("Obverse", omit({ ...type.obverse, id: type.id }), "id");
|
||||
inserter('Obverse', omit({ ...type.obverse, id: type.id }), 'id');
|
||||
}
|
||||
if (type.reverse) {
|
||||
inserter("Reverse", omit({ ...type.reverse, id: type.id }), "id");
|
||||
inserter('Reverse', omit({ ...type.reverse, id: type.id }), 'id');
|
||||
}
|
||||
if (type.edge) {
|
||||
inserter("Edge", omit({ ...type.edge, id: type.id }), "id");
|
||||
inserter('Edge', omit({ ...type.edge, id: type.id }), 'id');
|
||||
}
|
||||
if (type.watermark) {
|
||||
inserter("Watermark", omit({ ...type.watermark, id: type.id }), "id");
|
||||
inserter('Watermark', omit({ ...type.watermark, id: type.id }), 'id');
|
||||
}
|
||||
|
||||
inserter(
|
||||
"NumistaType",
|
||||
'NumistaType',
|
||||
omit(
|
||||
{
|
||||
...type,
|
||||
@@ -110,51 +96,51 @@ export const handleInsertType = (type: NumistaType) => {
|
||||
...(type.obverse && { obverse_id: type.id }),
|
||||
...(type.reverse && { reverse_id: type.id }),
|
||||
...(type.edge && { edge_id: type.id }),
|
||||
...(type.watermark && { watermark_id: type.id }),
|
||||
...(type.watermark && { watermark_id: type.id })
|
||||
},
|
||||
[
|
||||
"issuer",
|
||||
"value",
|
||||
"ruler",
|
||||
"obverse",
|
||||
"reverse",
|
||||
"edge",
|
||||
"watermark",
|
||||
"tags",
|
||||
"references",
|
||||
"mints",
|
||||
"printers",
|
||||
"demonetization",
|
||||
"composition",
|
||||
"technique",
|
||||
"related_types",
|
||||
],
|
||||
'issuer',
|
||||
'value',
|
||||
'ruler',
|
||||
'obverse',
|
||||
'reverse',
|
||||
'edge',
|
||||
'watermark',
|
||||
'tags',
|
||||
'references',
|
||||
'mints',
|
||||
'printers',
|
||||
'demonetization',
|
||||
'composition',
|
||||
'technique',
|
||||
'related_types'
|
||||
]
|
||||
),
|
||||
"id",
|
||||
'id'
|
||||
);
|
||||
|
||||
if (type.ruler?.length) {
|
||||
for (const ruler of type.ruler) {
|
||||
if (ruler.group) {
|
||||
inserter("RulerGroup", omit(ruler.group), "id");
|
||||
inserter('RulerGroup', omit(ruler.group), 'id');
|
||||
}
|
||||
|
||||
inserter(
|
||||
"Ruler",
|
||||
'Ruler',
|
||||
omit(
|
||||
{
|
||||
...ruler,
|
||||
group_id: ruler.group?.id,
|
||||
group_id: ruler.group?.id
|
||||
},
|
||||
["group"],
|
||||
['group']
|
||||
),
|
||||
"id",
|
||||
'id'
|
||||
);
|
||||
|
||||
try {
|
||||
inserter("TypeRuler", {
|
||||
inserter('TypeRuler', {
|
||||
type_id: type.id,
|
||||
ruler_id: ruler.id,
|
||||
ruler_id: ruler.id
|
||||
});
|
||||
} catch {
|
||||
//ok
|
||||
@@ -164,7 +150,7 @@ export const handleInsertType = (type: NumistaType) => {
|
||||
};
|
||||
|
||||
export const handleUpdateTypeCount = (type: NumistaType) => {
|
||||
if (!exists("NumistaType", "id", type.id)) {
|
||||
if (!exists('NumistaType', 'id', type.id)) {
|
||||
console.error(`id ${type.id} does not exist`);
|
||||
return;
|
||||
}
|
||||
@@ -178,13 +164,13 @@ export const handleUpdateTypeCount = (type: NumistaType) => {
|
||||
export const handleRelated = (type: NumistaType) => {
|
||||
if (type.related_types?.length) {
|
||||
for (const related_type of type.related_types) {
|
||||
if (exists("NumistaType", "id", related_type.id)) {
|
||||
if (exists('NumistaType', 'id', related_type.id)) {
|
||||
inserter(
|
||||
"TypeRelated",
|
||||
'TypeRelated',
|
||||
omit({
|
||||
type1_id: type.id,
|
||||
type2_id: related_type.id,
|
||||
}),
|
||||
type2_id: related_type.id
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
const nextConfig = {
|
||||
redirects: () => [
|
||||
{
|
||||
destination: "/",
|
||||
destination: '/',
|
||||
permanent: true,
|
||||
source: "/showcase-banknotes",
|
||||
},
|
||||
source: '/showcase-banknotes'
|
||||
}
|
||||
],
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: true
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^13.5.4",
|
||||
"@tanstack/eslint-plugin-query": "^4.34.1",
|
||||
"@types/node": "20.6.0",
|
||||
"@types/react": "18.2.21",
|
||||
@@ -37,11 +38,14 @@
|
||||
"@types/three": "0.152.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@untile/eslint-config-typescript-react": "^2.0.1",
|
||||
"@untile/prettier-config": "^2.0.1",
|
||||
"bun-types": "^1.0.3",
|
||||
"eslint": "8.49.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
},
|
||||
"prettier": "@untile/prettier-config"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { prisma } from "@/api";
|
||||
import { prisma } from '@/api';
|
||||
|
||||
export const getCountries = async (filter?: string) => {
|
||||
return await prisma.issuer.findMany({
|
||||
...(filter && {
|
||||
where: {
|
||||
name: { contains: filter },
|
||||
},
|
||||
}),
|
||||
name: { contains: filter }
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,66 +1,62 @@
|
||||
import { prisma } from "@/api";
|
||||
import { prisma } from '@/api';
|
||||
|
||||
export const getCountryCounts = async (category?: "coins" | "banknotes") => {
|
||||
export const getCountryCounts = async (category?: 'coins' | 'banknotes') => {
|
||||
const countryBanknoteCounts =
|
||||
category !== "coins"
|
||||
category !== 'coins'
|
||||
? await prisma.numistaType.groupBy({
|
||||
by: ["issuer_code"],
|
||||
_sum: {
|
||||
count: true,
|
||||
},
|
||||
_count: true,
|
||||
_sum: {
|
||||
count: true
|
||||
},
|
||||
by: ['issuer_code'],
|
||||
where: {
|
||||
category: {
|
||||
equals: "banknote",
|
||||
},
|
||||
},
|
||||
equals: 'banknote'
|
||||
}
|
||||
}
|
||||
})
|
||||
: [];
|
||||
|
||||
const countryCoinCounts =
|
||||
category !== "banknotes"
|
||||
category !== 'banknotes'
|
||||
? await prisma.numistaType.groupBy({
|
||||
by: ["issuer_code"],
|
||||
_sum: {
|
||||
count: true,
|
||||
},
|
||||
_count: true,
|
||||
_sum: {
|
||||
count: true
|
||||
},
|
||||
by: ['issuer_code'],
|
||||
where: {
|
||||
category: {
|
||||
not: "banknote",
|
||||
},
|
||||
},
|
||||
not: 'banknote'
|
||||
}
|
||||
}
|
||||
})
|
||||
: [];
|
||||
|
||||
const countries = await prisma.issuer.findMany({
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
const map = countries.map((country) => {
|
||||
const banknoteCounts = countryBanknoteCounts.find(
|
||||
({ issuer_code }) => issuer_code === country.code,
|
||||
);
|
||||
const coinCounts = countryCoinCounts.find(
|
||||
({ issuer_code }) => issuer_code === country.code,
|
||||
);
|
||||
const map = countries.map(country => {
|
||||
const banknoteCounts = countryBanknoteCounts.find(({ issuer_code }) => issuer_code === country.code);
|
||||
const coinCounts = countryCoinCounts.find(({ issuer_code }) => issuer_code === country.code);
|
||||
|
||||
return {
|
||||
...country,
|
||||
...(banknoteCounts && {
|
||||
banknotes: {
|
||||
sum: banknoteCounts?._sum?.count,
|
||||
count: banknoteCounts?._count,
|
||||
},
|
||||
sum: banknoteCounts?._sum?.count
|
||||
}
|
||||
}),
|
||||
...(coinCounts && {
|
||||
coins: {
|
||||
sum: coinCounts?._sum?.count,
|
||||
count: coinCounts?._count,
|
||||
},
|
||||
}),
|
||||
sum: coinCounts?._sum?.count
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { prisma } from "@/api";
|
||||
import { prisma } from '@/api';
|
||||
|
||||
export const getCurrencies = async (filter?: string) => {
|
||||
return (
|
||||
await prisma.currency.groupBy({
|
||||
by: "name",
|
||||
by: 'name',
|
||||
...(filter && {
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
full_name: { contains: filter },
|
||||
name: { contains: filter },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
name: { contains: filter }
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
).map(({ name }) => name);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { prisma } from "@/api";
|
||||
import { include } from "./relations";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { include } from './relations';
|
||||
import { prisma } from '@/api';
|
||||
|
||||
export const getType = async (id: number) => {
|
||||
const type = await prisma.numistaType.findUnique({
|
||||
include,
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
id
|
||||
}
|
||||
});
|
||||
|
||||
return type as unknown as NumistaType | null;
|
||||
|
||||
@@ -1,70 +1,62 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@/api";
|
||||
import { include } from "./relations";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { include } from './relations';
|
||||
import { prisma } from '@/api';
|
||||
|
||||
type Props = {
|
||||
search?: string;
|
||||
category?: "coins" | "banknotes";
|
||||
category?: 'coins' | 'banknotes';
|
||||
countries?: string[];
|
||||
currencies?: string[];
|
||||
faceValue?: number;
|
||||
search?: string;
|
||||
special?: 'yes' | 'no' | 'both';
|
||||
yearRange?: [number, number];
|
||||
special?: "yes" | "no" | "both";
|
||||
};
|
||||
|
||||
export const getTypes = async ({
|
||||
search,
|
||||
category,
|
||||
countries,
|
||||
currencies,
|
||||
faceValue,
|
||||
yearRange,
|
||||
special,
|
||||
}: Props) => {
|
||||
export const getTypes = async ({ category, countries, currencies, faceValue, search, special, yearRange }: Props) => {
|
||||
const AND: Prisma.NumistaTypeWhereInput[] = [];
|
||||
const OR: Prisma.NumistaTypeWhereInput[] = [];
|
||||
|
||||
if (category === "banknotes") {
|
||||
if (category === 'banknotes') {
|
||||
AND.push({
|
||||
category: "banknote",
|
||||
category: 'banknote'
|
||||
});
|
||||
}
|
||||
|
||||
if (category === "coins") {
|
||||
if (category === 'coins') {
|
||||
AND.push({
|
||||
category: {
|
||||
not: "banknote",
|
||||
},
|
||||
not: 'banknote'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currencies?.length) {
|
||||
AND.push({
|
||||
value: {
|
||||
OR: currencies.map((currency) => ({
|
||||
OR: currencies.map(currency => ({
|
||||
currency: {
|
||||
OR: [{ name: currency }, { full_name: currency }],
|
||||
},
|
||||
})),
|
||||
},
|
||||
OR: [{ name: currency }, { full_name: currency }]
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (countries?.length) {
|
||||
AND.push({
|
||||
issuer: {
|
||||
OR: countries.map((country) => ({
|
||||
OR: countries.map(country => ({
|
||||
OR: [
|
||||
{
|
||||
code: country,
|
||||
code: country
|
||||
},
|
||||
{
|
||||
name: country,
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
name: country
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,40 +64,40 @@ export const getTypes = async ({
|
||||
AND.push({
|
||||
value: {
|
||||
numeric_value: {
|
||||
equals: faceValue,
|
||||
},
|
||||
},
|
||||
equals: faceValue
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (yearRange?.length === 2) {
|
||||
AND.push({
|
||||
AND: {
|
||||
min_year: {
|
||||
gte: yearRange[0],
|
||||
},
|
||||
max_year: {
|
||||
lte: yearRange[1],
|
||||
lte: yearRange[1]
|
||||
},
|
||||
},
|
||||
min_year: {
|
||||
gte: yearRange[0]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (special === "yes") {
|
||||
if (special === 'yes') {
|
||||
AND.push({
|
||||
NOT: {
|
||||
type: {
|
||||
contains: "Standard",
|
||||
},
|
||||
},
|
||||
contains: 'Standard'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (special === "no") {
|
||||
if (special === 'no') {
|
||||
AND.push({
|
||||
type: {
|
||||
contains: "Standard",
|
||||
},
|
||||
contains: 'Standard'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,23 +107,21 @@ export const getTypes = async ({
|
||||
issuer: {
|
||||
OR: [
|
||||
{
|
||||
code: { contains: search },
|
||||
code: { contains: search }
|
||||
},
|
||||
{
|
||||
name: { contains: search },
|
||||
},
|
||||
],
|
||||
},
|
||||
name: { contains: search }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
OR.push({
|
||||
value: {
|
||||
currency: {
|
||||
OR: [
|
||||
{ name: { contains: search } },
|
||||
{ full_name: { contains: search } },
|
||||
],
|
||||
},
|
||||
},
|
||||
OR: [{ name: { contains: search } }, { full_name: { contains: search } }]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,23 +134,23 @@ export const getTypes = async ({
|
||||
...(OR.length > 0
|
||||
? {
|
||||
where: {
|
||||
OR,
|
||||
},
|
||||
OR
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
orderBy: [
|
||||
{
|
||||
min_year: "asc",
|
||||
min_year: 'asc'
|
||||
},
|
||||
{
|
||||
max_year: "asc",
|
||||
max_year: 'asc'
|
||||
},
|
||||
{
|
||||
value: {
|
||||
numeric_value: "asc",
|
||||
},
|
||||
},
|
||||
],
|
||||
numeric_value: 'asc'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return types as unknown as NumistaType[];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
export const include: Prisma.NumistaTypeInclude = {
|
||||
edge: true,
|
||||
issuer: true,
|
||||
obverse: true,
|
||||
reverse: true,
|
||||
edge: true,
|
||||
value: {
|
||||
include: {
|
||||
currency: true,
|
||||
},
|
||||
},
|
||||
currency: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import * as THREE from "three";
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import * as THREE from 'three';
|
||||
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
|
||||
const SafeMaterial = ({
|
||||
url,
|
||||
bumpMap,
|
||||
setWidth,
|
||||
side,
|
||||
url
|
||||
}: {
|
||||
url: string;
|
||||
bumpMap: THREE.Texture;
|
||||
setWidth?: Dispatch<SetStateAction<number>>;
|
||||
side: "obverse" | "reverse";
|
||||
side: 'obverse' | 'reverse';
|
||||
url: string;
|
||||
}) => {
|
||||
const texture: THREE.Texture = useLoader(THREE.TextureLoader, url);
|
||||
|
||||
const index = useMemo(() => {
|
||||
switch (side) {
|
||||
case "obverse":
|
||||
case 'obverse':
|
||||
return 4;
|
||||
|
||||
default:
|
||||
@@ -42,13 +42,13 @@ const SafeMaterial = ({
|
||||
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
color={"#888"}
|
||||
map={texture}
|
||||
attach={`material-${index}`}
|
||||
bumpMap={bumpMap}
|
||||
bumpScale={0.1}
|
||||
color={'#888'}
|
||||
map={texture}
|
||||
metalness={0}
|
||||
roughness={0.5}
|
||||
attach={"material-" + index}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -56,10 +56,7 @@ const SafeMaterial = ({
|
||||
export const BanknoteInstance = ({ obverse, reverse }: NumistaType) => {
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
const obverseBumpTexture: THREE.Texture = useLoader(
|
||||
THREE.TextureLoader,
|
||||
"/photos/seamless-paper-normal-map.jpg",
|
||||
);
|
||||
const obverseBumpTexture: THREE.Texture = useLoader(THREE.TextureLoader, '/photos/seamless-paper-normal-map.jpg');
|
||||
|
||||
obverseBumpTexture.repeat = new THREE.Vector2(1, 0.25);
|
||||
obverseBumpTexture.wrapS = THREE.RepeatWrapping;
|
||||
@@ -71,50 +68,35 @@ export const BanknoteInstance = ({ obverse, reverse }: NumistaType) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<boxGeometry attach="geometry" args={[width, 2, 0.005]} />
|
||||
<boxGeometry args={[width, 2, 0.005]} attach={'geometry'} />
|
||||
|
||||
{Array.from({ length: 4 }, (_, index) => (
|
||||
<meshStandardMaterial
|
||||
key={index}
|
||||
color={"#888"}
|
||||
metalness={0}
|
||||
roughness={0.5}
|
||||
attach={"material-" + index}
|
||||
/>
|
||||
<meshStandardMaterial attach={`material-${index}`} color={'#888'} key={index} metalness={0} roughness={0.5} />
|
||||
))}
|
||||
|
||||
{obverse?.picture ? (
|
||||
<SafeMaterial
|
||||
bumpMap={obverseBumpTexture}
|
||||
url={obverse.picture}
|
||||
side={"obverse"}
|
||||
setWidth={setWidth}
|
||||
/>
|
||||
<SafeMaterial bumpMap={obverseBumpTexture} setWidth={setWidth} side={'obverse'} url={obverse.picture} />
|
||||
) : (
|
||||
<meshStandardMaterial
|
||||
color={"#888"}
|
||||
attach={'material-4'}
|
||||
bumpMap={obverseBumpTexture}
|
||||
bumpScale={0.1}
|
||||
color={'#888'}
|
||||
metalness={0}
|
||||
roughness={0.5}
|
||||
attach="material-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{reverse?.picture ? (
|
||||
<SafeMaterial
|
||||
bumpMap={reverseBumpTexture}
|
||||
url={reverse.picture}
|
||||
side={"reverse"}
|
||||
/>
|
||||
<SafeMaterial bumpMap={reverseBumpTexture} side={'reverse'} url={reverse.picture} />
|
||||
) : (
|
||||
<meshStandardMaterial
|
||||
color={"#888"}
|
||||
attach={'material-5'}
|
||||
bumpMap={reverseBumpTexture}
|
||||
bumpScale={0.1}
|
||||
color={'#888'}
|
||||
metalness={0}
|
||||
roughness={0.5}
|
||||
attach="material-5"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useGLTF, useTexture } from '@react-three/drei';
|
||||
|
||||
export function BahamasCents10(coin: NumistaType) {
|
||||
// @ts-expect-error property `nodes` actually exists
|
||||
const { nodes } = useGLTF("/models/bahamas-cents-10.gltf");
|
||||
const texture = useTexture("/api/merge-textures/" + coin.id);
|
||||
const { nodes } = useGLTF('/models/bahamas-cents-10.gltf');
|
||||
const texture = useTexture(`/api/merge-textures/${coin.id}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={nodes.BahamasCents10.geometry} />
|
||||
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
|
||||
<meshStandardMaterial map={texture} metalness={1} roughness={0.5} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/bahamas-cents-10.gltf");
|
||||
useGLTF.preload('/models/bahamas-cents-10.gltf');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useGLTF, useTexture } from '@react-three/drei';
|
||||
|
||||
export function BahamasCents15(coin: NumistaType) {
|
||||
// @ts-expect-error property `nodes` actually exists
|
||||
const { nodes } = useGLTF("/models/bahamas-cents-15.gltf");
|
||||
const texture = useTexture("/api/merge-textures/" + coin.id);
|
||||
const { nodes } = useGLTF('/models/bahamas-cents-15.gltf');
|
||||
const texture = useTexture(`/api/merge-textures/${coin.id}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={nodes.BahamasCents15.geometry} />
|
||||
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
|
||||
<meshStandardMaterial map={texture} metalness={1} roughness={0.5} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/bahamas-cents-15.gltf");
|
||||
useGLTF.preload('/models/bahamas-cents-15.gltf');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useGLTF, useTexture } from '@react-three/drei';
|
||||
|
||||
export function EuroCents20(coin: NumistaType) {
|
||||
// @ts-expect-error property `nodes` actually exists
|
||||
const { nodes } = useGLTF("/models/euro-cents-20.gltf");
|
||||
const texture = useTexture("/api/merge-textures/" + coin.id);
|
||||
const { nodes } = useGLTF('/models/euro-cents-20.gltf');
|
||||
const texture = useTexture(`/api/merge-textures/${coin.id}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={nodes.EuroCents20.geometry} />
|
||||
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
|
||||
<meshStandardMaterial map={texture} metalness={1} roughness={0.5} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/euro-cents-20.gltf");
|
||||
useGLTF.preload('/models/euro-cents-20.gltf');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useGLTF, useTexture } from '@react-three/drei';
|
||||
|
||||
export function TostaoFurado(coin: NumistaType) {
|
||||
// @ts-expect-error property `nodes` actually exists
|
||||
const { nodes } = useGLTF("/models/tostao-furado.gltf");
|
||||
const texture = useTexture("/api/merge-textures/" + coin.id);
|
||||
const { nodes } = useGLTF('/models/tostao-furado.gltf');
|
||||
const texture = useTexture(`/api/merge-textures/${coin.id}`);
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={nodes.TostaoFurado.geometry} />
|
||||
<meshStandardMaterial metalness={1} roughness={0.5} map={texture} />
|
||||
<meshStandardMaterial map={texture} metalness={1} roughness={0.5} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/tostao-furado.gltf");
|
||||
useGLTF.preload('/models/tostao-furado.gltf');
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import * as THREE from "three";
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { EuroCents20 } from "./custom/euro-cents-20";
|
||||
import { BahamasCents15 } from "./custom/bahamas-cents-15";
|
||||
import { BahamasCents10 } from "./custom/bahamas-cents-10";
|
||||
import { TostaoFurado } from "./custom/tostao-furado";
|
||||
import * as THREE from 'three';
|
||||
import { BahamasCents10 } from './custom/bahamas-cents-10';
|
||||
import { BahamasCents15 } from './custom/bahamas-cents-15';
|
||||
import { EuroCents20 } from './custom/euro-cents-20';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { TostaoFurado } from './custom/tostao-furado';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
|
||||
const SafeMaterial = ({
|
||||
url,
|
||||
orientation,
|
||||
side,
|
||||
}: Pick<NumistaType, "orientation"> & {
|
||||
url
|
||||
}: Pick<NumistaType, 'orientation'> & {
|
||||
side: 'edge' | 'obverse' | 'reverse';
|
||||
url: string;
|
||||
side: "edge" | "obverse" | "reverse";
|
||||
}) => {
|
||||
const texture: THREE.Texture = useLoader(THREE.TextureLoader, url);
|
||||
|
||||
const index = useMemo(() => {
|
||||
switch (side) {
|
||||
case "edge":
|
||||
case 'edge':
|
||||
return 0;
|
||||
|
||||
case "obverse":
|
||||
case 'obverse':
|
||||
return 1;
|
||||
|
||||
default:
|
||||
@@ -32,18 +32,19 @@ const SafeMaterial = ({
|
||||
|
||||
useEffect(() => {
|
||||
switch (side) {
|
||||
case "edge":
|
||||
case 'edge':
|
||||
texture.repeat = new THREE.Vector2(4, 1);
|
||||
texture.offset = new THREE.Vector2(0.5, 0);
|
||||
texture.wrapS = THREE.MirroredRepeatWrapping;
|
||||
texture.wrapT = THREE.MirroredRepeatWrapping;
|
||||
break;
|
||||
|
||||
case "reverse":
|
||||
if (orientation === "medal") {
|
||||
case 'reverse':
|
||||
if (orientation === 'medal') {
|
||||
texture.center = new THREE.Vector2(0.5, 0.5);
|
||||
texture.rotation = Math.PI;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -51,59 +52,42 @@ const SafeMaterial = ({
|
||||
}
|
||||
}, [orientation, texture, side]);
|
||||
|
||||
return (
|
||||
<meshStandardMaterial
|
||||
map={texture}
|
||||
metalness={1}
|
||||
roughness={0.5}
|
||||
attach={"material-" + index}
|
||||
/>
|
||||
);
|
||||
return <meshStandardMaterial attach={`material-${index}`} map={texture} metalness={1} roughness={0.5} />;
|
||||
};
|
||||
|
||||
export const BaseCoin = ({
|
||||
orientation,
|
||||
color,
|
||||
obverse,
|
||||
reverse,
|
||||
edge,
|
||||
}: NumistaType) => {
|
||||
export const BaseCoin = ({ color, edge, obverse, orientation, reverse }: NumistaType) => {
|
||||
const sides = useMemo(() => {
|
||||
//const parsed = Number(/(\d+)/.exec(shape)?.[0]);
|
||||
//return Math.min(32, Math.max(4, isNaN(parsed) ? 32 : parsed));
|
||||
// Const parsed = Number(/(\d+)/.exec(shape)?.[0]);
|
||||
// return Math.min(32, Math.max(4, isNaN(parsed) ? 32 : parsed));
|
||||
return 32;
|
||||
}, []);
|
||||
|
||||
const defaultProps = {
|
||||
metalness: 1,
|
||||
roughness: 0.5,
|
||||
color,
|
||||
metalness: 1,
|
||||
roughness: 0.5
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<cylinderGeometry attach="geometry" args={[1, 1, 1, sides]} />
|
||||
<cylinderGeometry args={[1, 1, 1, sides]} attach={'geometry'} />
|
||||
|
||||
{edge?.picture ? (
|
||||
<SafeMaterial url={edge.picture} side={"edge"} />
|
||||
<SafeMaterial side={'edge'} url={edge.picture} />
|
||||
) : (
|
||||
<meshStandardMaterial {...defaultProps} attach="material-0" />
|
||||
<meshStandardMaterial {...defaultProps} attach={'material-0'} />
|
||||
)}
|
||||
|
||||
{obverse?.picture ? (
|
||||
<SafeMaterial url={obverse.picture} side={"obverse"} />
|
||||
<SafeMaterial side={'obverse'} url={obverse.picture} />
|
||||
) : (
|
||||
<meshStandardMaterial {...defaultProps} attach="material-1" />
|
||||
<meshStandardMaterial {...defaultProps} attach={'material-1'} />
|
||||
)}
|
||||
|
||||
{reverse?.picture ? (
|
||||
<SafeMaterial
|
||||
url={reverse.picture}
|
||||
side={"reverse"}
|
||||
orientation={orientation}
|
||||
/>
|
||||
<SafeMaterial orientation={orientation} side={'reverse'} url={reverse.picture} />
|
||||
) : (
|
||||
<meshStandardMaterial {...defaultProps} attach="material-2" />
|
||||
<meshStandardMaterial {...defaultProps} attach={'material-2'} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -111,10 +95,9 @@ export const BaseCoin = ({
|
||||
|
||||
export const CoinInstance = (coin: NumistaType) => {
|
||||
if (
|
||||
(coin.value?.numeric_value === 0.2 &&
|
||||
coin.value?.currency?.name === "Euro") ||
|
||||
(coin.value?.numeric_value === 0.2 && coin.value?.currency?.name === 'Euro') ||
|
||||
(coin.value?.numeric_value === 50 &&
|
||||
coin.value?.currency?.name === "Peseta" &&
|
||||
coin.value?.currency?.name === 'Peseta' &&
|
||||
coin.min_year &&
|
||||
coin.min_year >= 1990)
|
||||
) {
|
||||
@@ -123,22 +106,21 @@ export const CoinInstance = (coin: NumistaType) => {
|
||||
|
||||
if (
|
||||
coin.value?.numeric_value === 25 &&
|
||||
coin.value?.currency?.name === "Peseta" &&
|
||||
coin.value?.currency?.name === 'Peseta' &&
|
||||
coin.min_year &&
|
||||
coin.min_year >= 1990
|
||||
) {
|
||||
return <TostaoFurado {...coin} />;
|
||||
}
|
||||
|
||||
if (
|
||||
coin.value?.currency?.name === "Dollar" &&
|
||||
coin.issuer?.code === "bahamas"
|
||||
) {
|
||||
if (coin.value?.currency?.name === 'Dollar' && coin.issuer?.code === 'bahamas') {
|
||||
switch (coin.value?.numeric_value) {
|
||||
case 0.15:
|
||||
return <BahamasCents15 {...coin} />;
|
||||
|
||||
case 0.1:
|
||||
return <BahamasCents10 {...coin} />;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo, useReducer } from "react";
|
||||
import { Slider } from "primereact/slider";
|
||||
import { InputNumber } from "primereact/inputnumber";
|
||||
import { useRouter } from "next/router";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import { MultiToggleFilter } from "@/components/filters/multi-toggle";
|
||||
import { Sidebar, SidebarProps } from "primereact/sidebar";
|
||||
import { MultiSelectCurrenciesFilter } from "@/components/filters/multi-select-currencies";
|
||||
import { MultiSelectCountriesFilter } from "./multi-select-countries";
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { MultiSelectCountriesFilter } from './multi-select-countries';
|
||||
import { MultiSelectCurrenciesFilter } from '@/components/filters/multi-select-currencies';
|
||||
import { MultiToggleFilter } from '@/components/filters/multi-toggle';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import { Sidebar, SidebarProps } from 'primereact/sidebar';
|
||||
import { Slider } from 'primereact/slider';
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
type Props = SidebarProps & {
|
||||
filterBanknotes?: boolean;
|
||||
@@ -14,92 +14,99 @@ type Props = SidebarProps & {
|
||||
};
|
||||
|
||||
type State = {
|
||||
search: string;
|
||||
faceValue?: number;
|
||||
banknotes: boolean;
|
||||
coins: boolean;
|
||||
countries: string[];
|
||||
currencies: string[];
|
||||
faceValue?: number;
|
||||
search: string;
|
||||
special: boolean;
|
||||
standard: boolean;
|
||||
coins: boolean;
|
||||
banknotes: boolean;
|
||||
yearRange: [number, number];
|
||||
};
|
||||
|
||||
type Action =
|
||||
| {
|
||||
key: "currency";
|
||||
key: 'currency';
|
||||
payload: string;
|
||||
}
|
||||
| {
|
||||
key: "country";
|
||||
key: 'country';
|
||||
payload: string;
|
||||
}
|
||||
| {
|
||||
key: "special" | "standard" | "coins" | "banknotes";
|
||||
key: 'special' | 'standard' | 'coins' | 'banknotes';
|
||||
payload?: boolean;
|
||||
}
|
||||
| {
|
||||
key: "yearRange";
|
||||
key: 'yearRange';
|
||||
payload?: number | [number, number];
|
||||
}
|
||||
| {
|
||||
key: "faceValue";
|
||||
key: 'faceValue';
|
||||
payload?: number | [number, number] | null;
|
||||
};
|
||||
|
||||
const validFaceValues = [
|
||||
0.01, 0.02, 0.05, 0.1, 0.2, 0.25, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 10, 15, 20,
|
||||
25, 50, 100, 200, 250, 500, 1000,
|
||||
0.01, 0.02, 0.05, 0.1, 0.2, 0.25, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 10, 15, 20, 25, 50, 100, 200, 250, 500, 1000
|
||||
];
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const arrayToggle = <T,>(array: T[], value: T) => {
|
||||
const arrayToggle = <T extends string>(array: T[], value: T) => {
|
||||
if (array.includes(value)) {
|
||||
return array.filter((inner) => inner !== value);
|
||||
return array.filter(inner => inner !== value);
|
||||
}
|
||||
|
||||
return [...array, value];
|
||||
};
|
||||
|
||||
const reducer = (state: State, { key, payload }: Action) => {
|
||||
switch (key) {
|
||||
case "currency":
|
||||
case 'currency':
|
||||
return {
|
||||
...state,
|
||||
currencies: arrayToggle(state.currencies, payload),
|
||||
currencies: arrayToggle(state.currencies, payload)
|
||||
};
|
||||
case "country":
|
||||
|
||||
case 'country':
|
||||
return {
|
||||
...state,
|
||||
countries: arrayToggle(state.countries, payload),
|
||||
countries: arrayToggle(state.countries, payload)
|
||||
};
|
||||
case "special":
|
||||
case "standard":
|
||||
case "coins":
|
||||
case "banknotes":
|
||||
|
||||
case 'special':
|
||||
case 'standard':
|
||||
case 'coins':
|
||||
case 'banknotes':
|
||||
return {
|
||||
...state,
|
||||
[key]: !!payload,
|
||||
[key]: !!payload
|
||||
};
|
||||
case "yearRange":
|
||||
|
||||
case 'yearRange':
|
||||
return {
|
||||
...state,
|
||||
...(Array.isArray(payload) && { yearRange: payload }),
|
||||
...(Array.isArray(payload) && { yearRange: payload })
|
||||
};
|
||||
case "faceValue":
|
||||
|
||||
case 'faceValue':
|
||||
if (Array.isArray(payload)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
return {
|
||||
...state,
|
||||
faceValue: undefined,
|
||||
faceValue: undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
faceValue: payload,
|
||||
faceValue: payload
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -107,218 +114,218 @@ const reducer = (state: State, { key, payload }: Action) => {
|
||||
|
||||
const stringifyState = (state: State) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (state.search.length) {
|
||||
searchParams.set("search", state.search);
|
||||
searchParams.set('search', state.search);
|
||||
}
|
||||
state.currencies.forEach((currency) => {
|
||||
searchParams.append("currencies", currency);
|
||||
|
||||
state.currencies.forEach(currency => {
|
||||
searchParams.append('currencies', currency);
|
||||
});
|
||||
state.countries.forEach((country) => {
|
||||
searchParams.append("countries", country);
|
||||
|
||||
state.countries.forEach(country => {
|
||||
searchParams.append('countries', country);
|
||||
});
|
||||
|
||||
if (state.faceValue) {
|
||||
searchParams.set("faceValue", String(state.faceValue));
|
||||
searchParams.set('faceValue', String(state.faceValue));
|
||||
}
|
||||
|
||||
if (state.special && !state.standard) {
|
||||
searchParams.set("special", "yes");
|
||||
searchParams.set('special', 'yes');
|
||||
}
|
||||
|
||||
if (!state.special && state.standard) {
|
||||
searchParams.set("special", "no");
|
||||
searchParams.set('special', 'no');
|
||||
}
|
||||
|
||||
if (state.coins && !state.banknotes) {
|
||||
searchParams.set("category", "coins");
|
||||
searchParams.set('category', 'coins');
|
||||
}
|
||||
|
||||
if (!state.coins && state.banknotes) {
|
||||
searchParams.set("category", "banknotes");
|
||||
searchParams.set('category', 'banknotes');
|
||||
}
|
||||
|
||||
if (state.yearRange[0] !== 1800 || state.yearRange[1] !== currentYear) {
|
||||
searchParams.append("yearRange", String(state.yearRange[0]));
|
||||
searchParams.append("yearRange", String(state.yearRange[1]));
|
||||
searchParams.append('yearRange', String(state.yearRange[0]));
|
||||
searchParams.append('yearRange', String(state.yearRange[1]));
|
||||
}
|
||||
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
const parseArray = (value?: string | string[]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value ? [value] : [];
|
||||
};
|
||||
|
||||
const getDefaultParams = (query: ParsedUrlQuery) => {
|
||||
return {
|
||||
banknotes: !query.category || query.category === 'banknotes',
|
||||
coins: !query.category || query.category === 'coins',
|
||||
countries: parseArray(query.countries),
|
||||
currencies: parseArray(query.currencies),
|
||||
faceValue: validFaceValues.includes(Number(query.faceValue))
|
||||
? (101 * validFaceValues.indexOf(Number(query.faceValue))) / validFaceValues.length
|
||||
: undefined,
|
||||
search: typeof query.search === 'string' ? query.search : '',
|
||||
special: !query.special || query.special === 'yes',
|
||||
standard: !query.special || query.special === 'no',
|
||||
yearRange: (() => {
|
||||
if (Array.isArray(query.yearRange)) {
|
||||
return [
|
||||
Math.max(1800, Math.min(currentYear, Number(query.yearRange[0]))),
|
||||
Math.min(currentYear, Math.max(1800, Number(query.yearRange[1]))),
|
||||
Math.min(currentYear, Math.max(1800, Number(query.yearRange[1])))
|
||||
];
|
||||
}
|
||||
|
||||
return [1800, currentYear];
|
||||
})() as [number, number],
|
||||
standard: !query.special || query.special === "no",
|
||||
special: !query.special || query.special === "yes",
|
||||
coins: !query.category || query.category === "coins",
|
||||
banknotes: !query.category || query.category === "banknotes",
|
||||
currencies: Array.isArray(query.currencies)
|
||||
? query.currencies
|
||||
: query.currencies
|
||||
? [query.currencies]
|
||||
: [],
|
||||
countries: Array.isArray(query.countries)
|
||||
? query.countries
|
||||
: query.countries
|
||||
? [query.countries]
|
||||
: [],
|
||||
search: typeof query.search === "string" ? query.search : "",
|
||||
faceValue: validFaceValues.includes(Number(query.faceValue))
|
||||
? (101 * validFaceValues.indexOf(Number(query.faceValue))) /
|
||||
validFaceValues.length
|
||||
: undefined,
|
||||
})() as [number, number]
|
||||
};
|
||||
};
|
||||
|
||||
export const Filters = ({ query, filterBanknotes, ...sidebarProps }: Props) => {
|
||||
export const Filters = ({ filterBanknotes, query, ...sidebarProps }: Props) => {
|
||||
const router = useRouter();
|
||||
const [state, dispatch] = useReducer(reducer, getDefaultParams(query));
|
||||
|
||||
const [validFaceValue, stringifiedState] = useMemo(() => {
|
||||
const validFaceValue = state.faceValue
|
||||
? validFaceValues[
|
||||
Math.floor((validFaceValues.length * state.faceValue) / 101)
|
||||
]
|
||||
? validFaceValues[Math.floor((validFaceValues.length * state.faceValue) / 101)]
|
||||
: undefined;
|
||||
return [
|
||||
validFaceValue,
|
||||
stringifyState({ ...state, faceValue: validFaceValue }),
|
||||
];
|
||||
|
||||
return [validFaceValue, stringifyState({ ...state, faceValue: validFaceValue })];
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.asPath !== router.pathname + "?" + stringifiedState) {
|
||||
router.replace(router.pathname + "?" + stringifiedState, undefined, {
|
||||
shallow: true,
|
||||
if (router.asPath !== `${router.pathname}?${stringifiedState}`) {
|
||||
router.replace(`${router.pathname}?${stringifiedState}`, undefined, {
|
||||
shallow: true
|
||||
});
|
||||
}
|
||||
}, [router, stringifiedState]);
|
||||
|
||||
return (
|
||||
<Sidebar position={"right"} {...sidebarProps}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<Sidebar position={'right'} {...sidebarProps}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
{!filterBanknotes && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<MultiToggleFilter
|
||||
baseName="type"
|
||||
baseName={'type'}
|
||||
items={[
|
||||
{
|
||||
name: "coins",
|
||||
value: state.coins,
|
||||
set: (payload) => dispatch({ key: "coins", payload }),
|
||||
name: 'coins',
|
||||
set: payload => dispatch({ key: 'coins', payload }),
|
||||
value: state.coins
|
||||
},
|
||||
{
|
||||
name: "banknotes",
|
||||
value: state.banknotes,
|
||||
set: (payload) => dispatch({ key: "banknotes", payload }),
|
||||
},
|
||||
name: 'banknotes',
|
||||
set: payload => dispatch({ key: 'banknotes', payload }),
|
||||
value: state.banknotes
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<MultiSelectCountriesFilter
|
||||
selectedValues={state.countries}
|
||||
toggleValue={(payload) => dispatch({ key: "country", payload })}
|
||||
toggleValue={payload => dispatch({ key: 'country', payload })}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<MultiSelectCurrenciesFilter
|
||||
selectedValues={state.currencies}
|
||||
toggleValue={(payload) => dispatch({ key: "currency", payload })}
|
||||
toggleValue={payload => dispatch({ key: 'currency', payload })}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<label htmlFor="faceValue">Face value</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<label htmlFor={'faceValue'}>{'Face value'}</label>
|
||||
<InputNumber
|
||||
style={{ marginBottom: 8, width: "100%", cursor: "not-allowed" }}
|
||||
minFractionDigits={2}
|
||||
onChange={() => null}
|
||||
pt={{
|
||||
input: {
|
||||
root: { style: { width: "100%", pointerEvents: "none" } },
|
||||
},
|
||||
root: { style: { pointerEvents: 'none', width: '100%' } }
|
||||
}
|
||||
}}
|
||||
minFractionDigits={2}
|
||||
style={{ cursor: 'not-allowed', marginBottom: 8, width: '100%' }}
|
||||
value={validFaceValue}
|
||||
onChange={() => null}
|
||||
/>
|
||||
<Slider
|
||||
name="faceValue"
|
||||
name={'faceValue'}
|
||||
onChange={({ value }) => dispatch({ key: 'faceValue', payload: value })}
|
||||
value={state.faceValue}
|
||||
onChange={({ value }) =>
|
||||
dispatch({ key: "faceValue", payload: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<MultiToggleFilter
|
||||
baseName="edition"
|
||||
baseName={'edition'}
|
||||
items={[
|
||||
{
|
||||
name: "special",
|
||||
value: state.special,
|
||||
set: (payload) => dispatch({ key: "special", payload }),
|
||||
name: 'special',
|
||||
set: payload => dispatch({ key: 'special', payload }),
|
||||
value: state.special
|
||||
},
|
||||
{
|
||||
name: "standard",
|
||||
value: state.standard,
|
||||
set: (payload) => dispatch({ key: "standard", payload }),
|
||||
},
|
||||
name: 'standard',
|
||||
set: payload => dispatch({ key: 'standard', payload }),
|
||||
value: state.standard
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<label htmlFor="years">Years</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<label htmlFor={'years'}>{'Years'}</label>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
alignItems: 'center',
|
||||
display: 'grid',
|
||||
gap: 16,
|
||||
gridTemplateColumns: "1fr max-content 1fr",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
gridTemplateColumns: '1fr max-content 1fr',
|
||||
marginBottom: 8
|
||||
}}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ cursor: "not-allowed" }}
|
||||
format={false}
|
||||
onChange={() => null}
|
||||
pt={{
|
||||
input: {
|
||||
root: { style: { width: "100%", pointerEvents: "none" } },
|
||||
},
|
||||
root: { style: { pointerEvents: 'none', width: '100%' } }
|
||||
}
|
||||
}}
|
||||
format={false}
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
value={state.yearRange[0]}
|
||||
onChange={() => null}
|
||||
/>
|
||||
{"-"}
|
||||
{'-'}
|
||||
<InputNumber
|
||||
style={{ cursor: "not-allowed" }}
|
||||
format={false}
|
||||
onChange={() => null}
|
||||
pt={{
|
||||
input: {
|
||||
root: {
|
||||
style: {
|
||||
width: "100%",
|
||||
textAlign: "right",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
pointerEvents: 'none',
|
||||
textAlign: 'right',
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
format={false}
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
value={state.yearRange[1]}
|
||||
onChange={() => null}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
name="years"
|
||||
value={state.yearRange}
|
||||
onChange={({ value }) =>
|
||||
dispatch({ key: "yearRange", payload: value })
|
||||
}
|
||||
range
|
||||
min={1800}
|
||||
max={currentYear}
|
||||
min={1800}
|
||||
name={'years'}
|
||||
onChange={({ value }) => dispatch({ key: 'yearRange', payload: value })}
|
||||
range
|
||||
value={state.yearRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getFlagEmoji } from "@/core/utils/flags";
|
||||
import { Issuer } from "@prisma/client";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { MultiSelect, MultiSelectProps } from "primereact/multiselect";
|
||||
import { useMemo } from "react";
|
||||
import { Issuer } from '@prisma/client';
|
||||
import { MultiSelect, MultiSelectProps } from 'primereact/multiselect';
|
||||
import { getFlagEmoji } from '@/core/utils/flags';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
type Props = MultiSelectProps & {
|
||||
selectedValues: string[];
|
||||
@@ -11,56 +11,47 @@ type Props = MultiSelectProps & {
|
||||
|
||||
const Option = (name: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
const country = queryClient
|
||||
.getQueryData<Issuer[]>(["countries"])
|
||||
?.find((country) => country.name === name);
|
||||
const country = queryClient.getQueryData<Issuer[]>(['countries'])?.find(country => country.name === name);
|
||||
|
||||
return getFlagEmoji(country?.iso) + " " + name;
|
||||
return `${getFlagEmoji(country?.iso)} ${name}`;
|
||||
};
|
||||
|
||||
export const MultiSelectCountriesFilter = ({
|
||||
selectedValues,
|
||||
toggleValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
export const MultiSelectCountriesFilter = ({ selectedValues, toggleValue, ...props }: Props) => {
|
||||
const { data } = useQuery<Issuer[]>({
|
||||
queryKey: ["countries"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/countries");
|
||||
const res = await fetch('/api/countries');
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
queryKey: ['countries']
|
||||
});
|
||||
|
||||
const countries = useMemo(
|
||||
() => data?.map(({ iso, name }) => getFlagEmoji(iso) + " " + name),
|
||||
[data],
|
||||
);
|
||||
const countries = useMemo(() => data?.map(({ iso, name }) => `${getFlagEmoji(iso)} ${name}`), [data]);
|
||||
const selectedWithFlags = useMemo(
|
||||
() =>
|
||||
selectedValues.map(
|
||||
(name) => {
|
||||
const country = data?.find((country) => country.name === name);
|
||||
return getFlagEmoji(country?.iso) + " " + name;
|
||||
name => {
|
||||
const country = data?.find(country => country.name === name);
|
||||
|
||||
return `${getFlagEmoji(country?.iso)} ${name}`;
|
||||
},
|
||||
[data],
|
||||
[data]
|
||||
),
|
||||
[data, selectedValues],
|
||||
[data, selectedValues]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={"countries"}>{"Countries"}</label>
|
||||
<label htmlFor={'countries'}>{'Countries'}</label>
|
||||
<MultiSelect
|
||||
name={"countries"}
|
||||
display="chip"
|
||||
value={selectedWithFlags}
|
||||
onChange={(e) =>
|
||||
toggleValue(e.selectedOption.split(" ").slice(1).join(" "))
|
||||
}
|
||||
itemTemplate={Option}
|
||||
options={countries ?? selectedValues}
|
||||
display={'chip'}
|
||||
filter
|
||||
itemTemplate={Option}
|
||||
maxSelectedLabels={4}
|
||||
name={'countries'}
|
||||
onChange={e => toggleValue(e.selectedOption.split(' ').slice(1).join(' '))}
|
||||
options={countries ?? selectedValues}
|
||||
value={selectedWithFlags}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MultiSelect, MultiSelectProps } from "primereact/multiselect";
|
||||
import { MultiSelect, MultiSelectProps } from 'primereact/multiselect';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export type Props = MultiSelectProps & {
|
||||
selectedValues: string[];
|
||||
toggleValue: (value: string) => void;
|
||||
};
|
||||
|
||||
export const MultiSelectCurrenciesFilter = ({
|
||||
selectedValues,
|
||||
toggleValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
export const MultiSelectCurrenciesFilter = ({ selectedValues, toggleValue, ...props }: Props) => {
|
||||
const { data } = useQuery<string[]>({
|
||||
queryKey: ["currencies"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/currencies");
|
||||
const res = await fetch('/api/currencies');
|
||||
|
||||
return await res.json();
|
||||
},
|
||||
queryKey: ['currencies']
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={"currencies"}>{"Currencies"}</label>
|
||||
<label htmlFor={'currencies'}>{'Currencies'}</label>
|
||||
<MultiSelect
|
||||
name={"currencies"}
|
||||
display="chip"
|
||||
value={selectedValues}
|
||||
onChange={(e) => toggleValue(e.selectedOption)}
|
||||
options={data ?? selectedValues}
|
||||
display={'chip'}
|
||||
filter
|
||||
maxSelectedLabels={4}
|
||||
name={'currencies'}
|
||||
onChange={e => toggleValue(e.selectedOption)}
|
||||
options={data ?? selectedValues}
|
||||
value={selectedValues}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Checkbox } from "primereact/checkbox";
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
|
||||
type Props = {
|
||||
baseName: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
value: boolean;
|
||||
set: (value?: boolean) => void;
|
||||
value: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -13,18 +13,15 @@ export const MultiToggleFilter = ({ baseName, items }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<label>{baseName}</label>
|
||||
<div style={{ display: "flex", gap: 24 }}>
|
||||
{items.map(({ name, value, set }) => (
|
||||
<div
|
||||
key={name}
|
||||
style={{ display: "flex", gap: 8, alignItems: "center" }}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{items.map(({ name, set, value }) => (
|
||||
<div key={name} style={{ alignItems: 'center', display: 'flex', gap: 8 }}>
|
||||
<Checkbox
|
||||
checked={value}
|
||||
inputId={name}
|
||||
name={baseName}
|
||||
value={name}
|
||||
onChange={({ target }) => set(target.checked)}
|
||||
checked={value}
|
||||
value={name}
|
||||
/>
|
||||
<label htmlFor={name}>{name}</label>
|
||||
</div>
|
||||
|
||||
@@ -1,63 +1,64 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "primereact/button";
|
||||
import { Button } from 'primereact/button';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
||||
type Props = {
|
||||
route: "pile" | "showcase";
|
||||
onOpenDrawer?: () => void;
|
||||
route: 'pile' | 'showcase';
|
||||
};
|
||||
|
||||
export const Header = ({ route, onOpenDrawer }: Props) => {
|
||||
export const Header = ({ onOpenDrawer, route }: Props) => {
|
||||
const router = useRouter();
|
||||
const searchParams = router.asPath.split("?")[1] ?? "";
|
||||
const searchParams = router.asPath.split('?')[1] ?? '';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
top: 0,
|
||||
padding: 16,
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Link href={"/"} className="p-button p-component" style={{ gap: 8 }}>
|
||||
<i className="pi pi-arrow-left" />
|
||||
<Link className={'p-button p-component'} href={'/'} style={{ gap: 8 }}>
|
||||
<i className={'pi pi-arrow-left'} />
|
||||
</Link>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
{route !== "pile" && (
|
||||
{route !== 'pile' && (
|
||||
<Link
|
||||
className="p-button p-button-secondary p-component"
|
||||
style={{
|
||||
width: 128,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
className={'p-button p-button-secondary p-component'}
|
||||
href={`/pile?${searchParams}`}
|
||||
>
|
||||
{"Pile"}
|
||||
</Link>
|
||||
)}
|
||||
{route !== "showcase" && (
|
||||
<Link
|
||||
className="p-button p-button-secondary p-component"
|
||||
style={{
|
||||
width: 128,
|
||||
justifyContent: "center",
|
||||
justifyContent: 'center',
|
||||
width: 128
|
||||
}}
|
||||
href={`/showcase?${searchParams}`}
|
||||
>
|
||||
{"Showcase"}
|
||||
{'Pile'}
|
||||
</Link>
|
||||
)}
|
||||
<Button icon="pi pi-search" onClick={onOpenDrawer} />
|
||||
{route !== 'showcase' && (
|
||||
<Link
|
||||
className={'p-button p-button-secondary p-component'}
|
||||
href={`/showcase?${searchParams}`}
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
width: 128
|
||||
}}
|
||||
>
|
||||
{'Showcase'}
|
||||
</Link>
|
||||
)}
|
||||
<Button icon={'pi pi-search'} onClick={onOpenDrawer} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { CoinInstance } from "../coin";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { InstancedMesh } from "three";
|
||||
import { useCylinder } from "@react-three/cannon";
|
||||
import { CoinInstance } from '../coin';
|
||||
import { InstancedMesh } from 'three';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useCylinder } from '@react-three/cannon';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const CoinInstances = (coinProps: NumistaType) => {
|
||||
const didInit = useRef(false);
|
||||
@@ -15,38 +15,31 @@ export const CoinInstances = (coinProps: NumistaType) => {
|
||||
() => ({
|
||||
args: [(1 + size) / 16, (1 + size) / 16, thickness / 16, 6],
|
||||
mass: weight,
|
||||
position: [0, -3, 0],
|
||||
position: [0, -3, 0]
|
||||
}),
|
||||
useRef<InstancedMesh>(null),
|
||||
useRef<InstancedMesh>(null)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (didInit.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
didInit.current = true;
|
||||
Array.from({ length: count }).forEach((_, index) => {
|
||||
at(index).position.set(
|
||||
8 * (Math.random() * 2 - 1),
|
||||
Math.random() * 8 + 16,
|
||||
8 * (Math.random() * 2 - 1),
|
||||
);
|
||||
at(index).position.set(8 * (Math.random() * 2 - 1), Math.random() * 8 + 16, 8 * (Math.random() * 2 - 1));
|
||||
at(index).rotation.set(
|
||||
Math.PI * (Math.random() * 2 - 1),
|
||||
Math.PI * (Math.random() * 2 - 1),
|
||||
Math.PI * (Math.random() * 2 - 1),
|
||||
Math.PI * (Math.random() * 2 - 1)
|
||||
);
|
||||
|
||||
at(index).scaleOverride([size / 16, thickness / 16, size / 16]);
|
||||
});
|
||||
}, [at, count, size, thickness]);
|
||||
|
||||
return (
|
||||
<instancedMesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
ref={ref}
|
||||
args={[undefined, undefined, count]}
|
||||
>
|
||||
<instancedMesh args={[undefined, undefined, count]} castShadow receiveShadow ref={ref}>
|
||||
<CoinInstance {...coinProps} />
|
||||
</instancedMesh>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Canvas, SpotLightProps } from "@react-three/fiber";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { Suspense, useMemo, useRef } from "react";
|
||||
import { CoinInstances } from "./coin-instances";
|
||||
import { PlaneProps, Physics, usePlane } from "@react-three/cannon";
|
||||
import { Mesh, Vector3 } from "three";
|
||||
import styles from "./index.module.css";
|
||||
import { StageLightModel } from "./stage-light-model";
|
||||
import { useNumistaTypes } from "@/core/hooks/use-types";
|
||||
import { Message } from "primereact/message";
|
||||
import { Canvas, SpotLightProps } from '@react-three/fiber';
|
||||
import { CoinInstances } from './coin-instances';
|
||||
import { Mesh, Vector3 } from 'three';
|
||||
import { Message } from 'primereact/message';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Physics, PlaneProps, usePlane } from '@react-three/cannon';
|
||||
import { StageLightModel } from './stage-light-model';
|
||||
import { Suspense, useMemo, useRef } from 'react';
|
||||
import { useNumistaTypes } from '@/core/hooks/use-types';
|
||||
import styles from './index.module.css';
|
||||
|
||||
const Plane = (props: PlaneProps) => {
|
||||
const [ref] = usePlane(() => ({ ...props }), useRef<Mesh>(null));
|
||||
@@ -15,15 +15,15 @@ const Plane = (props: PlaneProps) => {
|
||||
return (
|
||||
<mesh receiveShadow ref={ref}>
|
||||
<circleGeometry args={[128]} />
|
||||
<meshStandardMaterial color={"#222"} metalness={0} roughness={1} />
|
||||
<meshStandardMaterial color={'#222'} metalness={0} roughness={1} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
const StageLight = (
|
||||
props: Omit<SpotLightProps, "position"> & {
|
||||
props: Omit<SpotLightProps, 'position'> & {
|
||||
position: readonly [x: number, y: number, z: number];
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const pos = new Vector3(...props.position);
|
||||
|
||||
@@ -31,15 +31,9 @@ const StageLight = (
|
||||
<>
|
||||
<spotLight {...props} castShadow penumbra={1} />
|
||||
<StageLightModel position={pos} />
|
||||
<mesh
|
||||
position={[
|
||||
pos.x + Math.abs(pos.x) / pos.x,
|
||||
(pos.y - 2) / 2,
|
||||
pos.z + Math.abs(pos.z) / pos.z,
|
||||
]}
|
||||
>
|
||||
<mesh position={[pos.x + Math.abs(pos.x) / pos.x, (pos.y - 2) / 2, pos.z + Math.abs(pos.z) / pos.z]}>
|
||||
<cylinderGeometry args={[1 / 3, 1 / 3, pos.y + 2, 6]} />
|
||||
<meshBasicMaterial color="black" />
|
||||
<meshBasicMaterial color={'black'} />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
@@ -56,55 +50,46 @@ export const Pile = () => {
|
||||
{count > 200 && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
inset: "8px 64px auto",
|
||||
maxWidth: "max-content",
|
||||
margin: "auto",
|
||||
inset: '8px 64px auto',
|
||||
margin: 'auto',
|
||||
maxWidth: 'max-content',
|
||||
position: 'absolute',
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
severity="error"
|
||||
severity={'error'}
|
||||
style={{
|
||||
width: "max-content",
|
||||
maxWidth: "calc(100vw - 128px)",
|
||||
margin: "auto",
|
||||
margin: 'auto',
|
||||
maxWidth: 'calc(100vw - 128px)',
|
||||
width: 'max-content'
|
||||
}}
|
||||
text={
|
||||
<>
|
||||
{"Existem " + count + " moedas que cumprem estes requisitos."}
|
||||
<br /> {"Faz uma pesquisa mais específica."}
|
||||
{`Existem ${count} moedas que cumprem estes requisitos.`}
|
||||
<br /> {'Faz uma pesquisa mais específica.'}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ position: [0, 16, 16] }}
|
||||
className={styles.canvas}
|
||||
>
|
||||
<Canvas camera={{ position: [0, 16, 16] }} className={styles.canvas} shadows>
|
||||
<StageLight position={[32, 16, 32]} />
|
||||
<StageLight position={[32, 16, -32]} />
|
||||
<StageLight position={[-32, 16, 32]} />
|
||||
<StageLight position={[-32, 16, -32]} />
|
||||
<Physics gravity={[0, -20, 0]}>
|
||||
<Plane rotation={[-Math.PI / 2, 0, 0]} position={[0, -2, 0]} />
|
||||
<Plane position={[0, -2, 0]} rotation={[-Math.PI / 2, 0, 0]} />
|
||||
{count < 200 &&
|
||||
coins?.map((coinProps) => (
|
||||
coins?.map(coinProps => (
|
||||
<Suspense fallback={null} key={coinProps.id}>
|
||||
<CoinInstances {...coinProps} />
|
||||
</Suspense>
|
||||
))}
|
||||
</Physics>
|
||||
|
||||
<OrbitControls
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
enablePan={false}
|
||||
minDistance={4}
|
||||
maxDistance={96}
|
||||
/>
|
||||
<OrbitControls enablePan={false} maxDistance={96} maxPolarAngle={Math.PI / 2} minDistance={4} />
|
||||
</Canvas>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
import { GroupProps } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { Group } from "three";
|
||||
import { Group } from 'three';
|
||||
import { GroupProps } from '@react-three/fiber';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
export function StageLightModel(props: GroupProps) {
|
||||
// @ts-expect-error properties `nodes` and `materials` actually exist
|
||||
const { nodes, materials } = useGLTF("/models/stage-light.gltf");
|
||||
const { materials, nodes } = useGLTF('/models/stage-light.gltf');
|
||||
const ref = useRef<Group>(null!);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
ref.current.lookAt(
|
||||
32 * (Math.random() - 0.5),
|
||||
32 * (Math.random() - 0.5),
|
||||
32 * (Math.random() - 0.5),
|
||||
);
|
||||
ref.current.lookAt(32 * (Math.random() - 0.5), 32 * (Math.random() - 0.5), 32 * (Math.random() - 0.5));
|
||||
ref.current.rotateY(-Math.PI / 2);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<group ref={ref} {...props} dispose={null}>
|
||||
<mesh
|
||||
position={[-2, 0, 0]}
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.Cube001.geometry}
|
||||
material={materials.Base}
|
||||
position={[-2, 0, 0]}
|
||||
receiveShadow
|
||||
/>
|
||||
<mesh
|
||||
position={[-2, 0, 0]}
|
||||
castShadow
|
||||
receiveShadow
|
||||
geometry={nodes.Cube001_1.geometry}
|
||||
material={materials.Light}
|
||||
position={[-2, 0, 0]}
|
||||
receiveShadow
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/stage-light.gltf");
|
||||
useGLTF.preload('/models/stage-light.gltf');
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import styles from "./index.module.css";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { useEffect } from "react";
|
||||
import { InfoBox } from "./info-box";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { Row } from "./row";
|
||||
import { useShowcaseStore } from "./store";
|
||||
import { useNumistaTypes } from "@/core/hooks/use-types";
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { InfoBox } from './info-box';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Row } from './row';
|
||||
import { useEffect } from 'react';
|
||||
import { useNumistaTypes } from '@/core/hooks/use-types';
|
||||
import { useShowcaseStore } from './store';
|
||||
import styles from './index.module.css';
|
||||
|
||||
export const Showcase = () => {
|
||||
const setTypes = useShowcaseStore((state) => state.setTypes);
|
||||
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||
const nextIndex = useShowcaseStore((state) => state.nextIndex);
|
||||
const previousIndex = useShowcaseStore((state) => state.previousIndex);
|
||||
const toggleSpin = useShowcaseStore((state) => state.toggleSpin);
|
||||
const setOrbitControlsRef = useShowcaseStore(
|
||||
(state) => state.setOrbitControlsRef,
|
||||
);
|
||||
const setTypes = useShowcaseStore(state => state.setTypes);
|
||||
const isSpinning = useShowcaseStore(state => state.isSpinning);
|
||||
const nextIndex = useShowcaseStore(state => state.nextIndex);
|
||||
const previousIndex = useShowcaseStore(state => state.previousIndex);
|
||||
const toggleSpin = useShowcaseStore(state => state.toggleSpin);
|
||||
const setOrbitControlsRef = useShowcaseStore(state => state.setOrbitControlsRef);
|
||||
|
||||
useNumistaTypes({ onSuccess: setTypes });
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = ({ key }: KeyboardEvent) => {
|
||||
switch (key) {
|
||||
case "ArrowLeft":
|
||||
case 'ArrowLeft':
|
||||
return previousIndex();
|
||||
case "ArrowRight":
|
||||
|
||||
case 'ArrowRight':
|
||||
return nextIndex();
|
||||
case " ":
|
||||
|
||||
case ' ':
|
||||
return toggleSpin();
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
|
||||
return () => document.removeEventListener('keydown', onKeyDown);
|
||||
}, [nextIndex, previousIndex, toggleSpin]);
|
||||
|
||||
return (
|
||||
@@ -42,11 +47,11 @@ export const Showcase = () => {
|
||||
<directionalLight position={[0, 0.125, -1]} />
|
||||
<Row />
|
||||
<OrbitControls
|
||||
ref={setOrbitControlsRef}
|
||||
enableRotate={!isSpinning}
|
||||
enablePan={false}
|
||||
minDistance={2}
|
||||
enableRotate={!isSpinning}
|
||||
maxDistance={10}
|
||||
minDistance={2}
|
||||
ref={setOrbitControlsRef}
|
||||
/>
|
||||
</Canvas>
|
||||
|
||||
|
||||
@@ -1,103 +1,94 @@
|
||||
import { Button } from "primereact/button";
|
||||
import { useShowcaseStore } from "./store";
|
||||
import { getFlagEmoji } from "@/core/utils/flags";
|
||||
import { Button } from 'primereact/button';
|
||||
import { getFlagEmoji } from '@/core/utils/flags';
|
||||
import { useShowcaseStore } from './store';
|
||||
|
||||
export const InfoBox = () => {
|
||||
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||
const getType = useShowcaseStore((state) => state.getType);
|
||||
const toggleSpin = useShowcaseStore((state) => state.toggleSpin);
|
||||
const currentIndex = useShowcaseStore((state) => state.currentIndex);
|
||||
const nextIndex = useShowcaseStore((state) => state.nextIndex);
|
||||
const previousIndex = useShowcaseStore((state) => state.previousIndex);
|
||||
const total = useShowcaseStore((state) => state.types.length);
|
||||
const isSpinning = useShowcaseStore(state => state.isSpinning);
|
||||
const getType = useShowcaseStore(state => state.getType);
|
||||
const toggleSpin = useShowcaseStore(state => state.toggleSpin);
|
||||
const currentIndex = useShowcaseStore(state => state.currentIndex);
|
||||
const nextIndex = useShowcaseStore(state => state.nextIndex);
|
||||
const previousIndex = useShowcaseStore(state => state.previousIndex);
|
||||
const total = useShowcaseStore(state => state.types.length);
|
||||
|
||||
const currentType = getType();
|
||||
|
||||
if (!currentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
issuer,
|
||||
category,
|
||||
title,
|
||||
min_year,
|
||||
max_year,
|
||||
count = 1,
|
||||
weight,
|
||||
size,
|
||||
thickness,
|
||||
} = currentType;
|
||||
const { category, count = 1, issuer, max_year, min_year, size, thickness, title, weight } = currentType;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
alignItems: 'center',
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
left: 0,
|
||||
right: 0,
|
||||
position: 'absolute',
|
||||
right: 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
alignItems: 'center',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
maxWidth: "100vw",
|
||||
width: 720,
|
||||
maxWidth: '100vw',
|
||||
padding: 16,
|
||||
color: "white",
|
||||
width: 720
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{getFlagEmoji(issuer?.iso) + " "}
|
||||
{`${getFlagEmoji(issuer?.iso)} `}
|
||||
<small>{issuer?.name}</small>
|
||||
</span>
|
||||
<h1 style={{ textAlign: "center" }}>{title}</h1>
|
||||
<p>{min_year !== max_year ? min_year + " - " + max_year : min_year}</p>
|
||||
<small>{(count ?? 1) + " in collection"}</small>
|
||||
{category === "coin" && (
|
||||
<h1 style={{ textAlign: 'center' }}>{title}</h1>
|
||||
<p>{min_year !== max_year ? `${min_year} - ${max_year}` : min_year}</p>
|
||||
<small>{`${count ?? 1} in collection`}</small>
|
||||
{category === 'coin' && (
|
||||
<>
|
||||
{weight && <p>{weight + "g"}</p>}
|
||||
{size && <p>{size + "mm"}</p>}
|
||||
{thickness && <p>{thickness + "mm"}</p>}
|
||||
{weight && <p>{`${weight}g`}</p>}
|
||||
{size && <p>{`${size}mm`}</p>}
|
||||
{thickness && <p>{`${thickness}mm`}</p>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginBottom: "8px" }}
|
||||
icon={`pi pi-${isSpinning ? 'pause' : 'play'}`}
|
||||
onClick={toggleSpin}
|
||||
icon={"pi pi-" + (isSpinning ? "pause" : "play")}
|
||||
size={'small'}
|
||||
style={{ marginBottom: '8px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
gap: 16,
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: 16
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
severity="secondary"
|
||||
size="small"
|
||||
disabled={currentIndex <= 0}
|
||||
icon={'pi pi-arrow-left'}
|
||||
onClick={previousIndex}
|
||||
icon={"pi pi-arrow-left"}
|
||||
severity={'secondary'}
|
||||
size={'small'}
|
||||
/>
|
||||
|
||||
<span>{currentIndex + 1 + "/" + total}</span>
|
||||
<span>{`${currentIndex + 1}/${total}`}</span>
|
||||
|
||||
<Button
|
||||
severity="secondary"
|
||||
size="small"
|
||||
disabled={currentIndex >= total - 1}
|
||||
icon={'pi pi-arrow-right'}
|
||||
onClick={nextIndex}
|
||||
icon={"pi pi-arrow-right"}
|
||||
severity={'secondary'}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { MeshProps, useFrame } from "@react-three/fiber";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
import { CoinInstance } from "@/components/coin";
|
||||
import { Mesh } from "three";
|
||||
import { BanknoteInstance } from "@/components/banknote";
|
||||
import { useShowcaseStore } from "./store";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { usePrevious } from "@/core/hooks/use-previous";
|
||||
import { BanknoteInstance } from '@/components/banknote';
|
||||
import { CoinInstance } from '@/components/coin';
|
||||
import { Mesh } from 'three';
|
||||
import { MeshProps, useFrame } from '@react-three/fiber';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import { usePrevious } from '@/core/hooks/use-previous';
|
||||
import { useShowcaseStore } from './store';
|
||||
|
||||
const SpinninType = ({
|
||||
typeProps,
|
||||
isSelected,
|
||||
typeProps,
|
||||
...props
|
||||
}: MeshProps & {
|
||||
typeProps?: NumistaType;
|
||||
isSelected?: boolean;
|
||||
typeProps?: NumistaType;
|
||||
}) => {
|
||||
const ref = useRef<Mesh>(null!);
|
||||
const isSpinning = useShowcaseStore((state) => state.isSpinning);
|
||||
const isTransitioning = useShowcaseStore((state) => state.isTransitioning);
|
||||
const Instance =
|
||||
typeProps?.category === "banknote" ? BanknoteInstance : CoinInstance;
|
||||
const isSpinning = useShowcaseStore(state => state.isSpinning);
|
||||
const isTransitioning = useShowcaseStore(state => state.isTransitioning);
|
||||
const Instance = typeProps?.category === 'banknote' ? BanknoteInstance : CoinInstance;
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!isSelected || !isSpinning || !typeProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeProps.category === "banknote") {
|
||||
if (typeProps.category === 'banknote') {
|
||||
ref.current.rotation.x += delta;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (typeProps.orientation) {
|
||||
case "medal":
|
||||
case 'medal':
|
||||
ref.current.rotation.x += delta;
|
||||
break;
|
||||
|
||||
@@ -53,14 +53,14 @@ const SpinninType = ({
|
||||
<mesh {...props}>
|
||||
<mesh
|
||||
ref={ref}
|
||||
{...(typeProps.category !== "banknote"
|
||||
{...(typeProps.category !== 'banknote'
|
||||
? {
|
||||
scale: [size / 16, thickness / 16, size / 16],
|
||||
scale: [size / 16, thickness / 16, size / 16]
|
||||
}
|
||||
: { rotation: [Math.PI / 2, Math.PI, Math.PI / 2] })}
|
||||
{...(!isSelected &&
|
||||
!isTransitioning && {
|
||||
scale: [0, 0, 0],
|
||||
scale: [0, 0, 0]
|
||||
})}
|
||||
>
|
||||
<Instance {...typeProps} />
|
||||
@@ -71,12 +71,12 @@ const SpinninType = ({
|
||||
|
||||
export const Row = () => {
|
||||
const ref = useRef<Mesh>(null!);
|
||||
const currentIndex = useShowcaseStore((state) => state.currentIndex);
|
||||
const getType = useShowcaseStore((state) => state.getType);
|
||||
const isTransitioning = useShowcaseStore((state) => state.isTransitioning);
|
||||
const endTransition = useShowcaseStore((state) => state.endTransition);
|
||||
const beginTransition = useShowcaseStore((state) => state.beginTransition);
|
||||
const types = useShowcaseStore((state) => state.types);
|
||||
const currentIndex = useShowcaseStore(state => state.currentIndex);
|
||||
const getType = useShowcaseStore(state => state.getType);
|
||||
const isTransitioning = useShowcaseStore(state => state.isTransitioning);
|
||||
const endTransition = useShowcaseStore(state => state.endTransition);
|
||||
const beginTransition = useShowcaseStore(state => state.beginTransition);
|
||||
const types = useShowcaseStore(state => state.types);
|
||||
const [spacing, setSpacing] = useState<number>(0);
|
||||
const delayedIndex = usePrevious(currentIndex);
|
||||
|
||||
@@ -85,11 +85,12 @@ export const Row = () => {
|
||||
setSpacing(Math.sqrt((48 * window.innerWidth) / window.innerHeight));
|
||||
beginTransition();
|
||||
};
|
||||
|
||||
updateSpacing();
|
||||
|
||||
window.addEventListener("resize", updateSpacing);
|
||||
window.addEventListener('resize', updateSpacing);
|
||||
|
||||
return () => window.removeEventListener("resize", updateSpacing);
|
||||
return () => window.removeEventListener('resize', updateSpacing);
|
||||
}, [beginTransition]);
|
||||
|
||||
useFrame(() => {
|
||||
@@ -112,15 +113,15 @@ export const Row = () => {
|
||||
{types?.map(
|
||||
(type, index) =>
|
||||
Math.abs(index - delayedIndex) <= 2 && (
|
||||
<Suspense key={type.id} fallback={null}>
|
||||
<Suspense fallback={null} key={type.id}>
|
||||
<SpinninType
|
||||
typeProps={getType(index)}
|
||||
isSelected={currentIndex === index}
|
||||
position={[index * spacing, 0, 0]}
|
||||
rotation={[Math.PI / 2, Math.PI / 2, 0]}
|
||||
typeProps={getType(index)}
|
||||
/>
|
||||
</Suspense>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</mesh>
|
||||
);
|
||||
|
||||
@@ -1,79 +1,81 @@
|
||||
import { create } from "zustand";
|
||||
import { OrbitControls } from "three-stdlib";
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { MutableRefObject, createRef } from "react";
|
||||
import { MutableRefObject, createRef } from 'react';
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { OrbitControls } from 'three-stdlib';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ShowcaseStore {
|
||||
beginTransition: () => void;
|
||||
currentIndex: number;
|
||||
endTransition: () => void;
|
||||
beginTransition: () => void;
|
||||
getType: (index?: number) => NumistaType | undefined;
|
||||
isSpinning: boolean;
|
||||
isTransitioning: boolean;
|
||||
nextIndex: () => void;
|
||||
previousIndex: () => void;
|
||||
orbitControlsRef: MutableRefObject<OrbitControls | null>;
|
||||
previousIndex: () => void;
|
||||
setOrbitControlsRef: (ref: OrbitControls | null) => void;
|
||||
setTypes: (types?: NumistaType[] | null) => void;
|
||||
toggleSpin: () => void;
|
||||
types: NumistaType[];
|
||||
setTypes: (types?: NumistaType[] | null) => void;
|
||||
getType: (index?: number) => NumistaType | undefined;
|
||||
}
|
||||
|
||||
export const useShowcaseStore = create<ShowcaseStore>((set, get) => ({
|
||||
beginTransition: () => set(() => ({ isTransitioning: true })),
|
||||
currentIndex: 0,
|
||||
endTransition: () => set(() => ({ isTransitioning: false })),
|
||||
getType: index => {
|
||||
const { currentIndex, types } = get();
|
||||
|
||||
return types[index ?? currentIndex];
|
||||
},
|
||||
isSpinning: true,
|
||||
isTransitioning: false,
|
||||
currentIndex: 0,
|
||||
types: [],
|
||||
setTypes: (types) =>
|
||||
set(() => ({
|
||||
isSpinning: true,
|
||||
isTransitioning: true,
|
||||
currentIndex: 0,
|
||||
types: Array.isArray(types) ? types : [],
|
||||
})),
|
||||
orbitControlsRef: createRef(),
|
||||
nextIndex: () =>
|
||||
set((state) => {
|
||||
set(state => {
|
||||
state.orbitControlsRef.current?.setAzimuthalAngle(0);
|
||||
state.orbitControlsRef.current?.setPolarAngle(Math.PI / 2);
|
||||
const currentIndex = Math.min(
|
||||
state.currentIndex + 1,
|
||||
state.types.length - 1,
|
||||
);
|
||||
const currentIndex = Math.min(state.currentIndex + 1, state.types.length - 1);
|
||||
|
||||
return {
|
||||
currentIndex,
|
||||
isTransitioning: true,
|
||||
currentType: state.types[currentIndex],
|
||||
isTransitioning: true
|
||||
};
|
||||
}),
|
||||
orbitControlsRef: createRef(),
|
||||
previousIndex: () =>
|
||||
set((state) => {
|
||||
set(state => {
|
||||
state.orbitControlsRef.current?.setAzimuthalAngle(0);
|
||||
state.orbitControlsRef.current?.setPolarAngle(Math.PI / 2);
|
||||
const currentIndex = Math.max(state.currentIndex - 1, 0);
|
||||
|
||||
return {
|
||||
currentIndex,
|
||||
isTransitioning: true,
|
||||
currentType: state.types[currentIndex],
|
||||
isTransitioning: true
|
||||
};
|
||||
}),
|
||||
setOrbitControlsRef: instance =>
|
||||
set(() => ({
|
||||
orbitControlsRef: { current: instance }
|
||||
})),
|
||||
setTypes: types =>
|
||||
set(() => ({
|
||||
currentIndex: 0,
|
||||
isSpinning: true,
|
||||
isTransitioning: true,
|
||||
types: Array.isArray(types) ? types : []
|
||||
})),
|
||||
toggleSpin: () =>
|
||||
set((state) => {
|
||||
set(state => {
|
||||
if (!state.isSpinning) {
|
||||
state.orbitControlsRef.current?.setAzimuthalAngle(0);
|
||||
state.orbitControlsRef.current?.setPolarAngle(Math.PI / 2);
|
||||
|
||||
return { isSpinning: true };
|
||||
}
|
||||
|
||||
return { isSpinning: false };
|
||||
}),
|
||||
endTransition: () => set(() => ({ isTransitioning: false })),
|
||||
beginTransition: () => set(() => ({ isTransitioning: true })),
|
||||
getType: (index) => {
|
||||
const { types, currentIndex } = get();
|
||||
return types[index ?? currentIndex];
|
||||
},
|
||||
setOrbitControlsRef: (instance) =>
|
||||
set(() => ({
|
||||
orbitControlsRef: { current: instance },
|
||||
})),
|
||||
types: []
|
||||
}));
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const breakpoints = {
|
||||
md: 992,
|
||||
ms: 768,
|
||||
sm: 576,
|
||||
sm: 576
|
||||
} as const;
|
||||
|
||||
type Breakpoint = keyof typeof breakpoints;
|
||||
|
||||
export function useMediaQuery(
|
||||
which: "max" | "min",
|
||||
breakpoint: Breakpoint,
|
||||
): boolean {
|
||||
export function useMediaQuery(which: 'max' | 'min', breakpoint: Breakpoint): boolean {
|
||||
const [matches, setMatches] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(
|
||||
`(${which}-width: ${breakpoints[breakpoint]}px)`,
|
||||
);
|
||||
const media = window.matchMedia(`(${which}-width: ${breakpoints[breakpoint]}px)`);
|
||||
const listener = () => setMatches(media.matches);
|
||||
listener();
|
||||
|
||||
media.addEventListener("change", listener);
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener("change", listener);
|
||||
media.removeEventListener('change', listener);
|
||||
};
|
||||
}, [breakpoint, which]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(value: T): T {
|
||||
const ref = useRef<T>(value);
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/router";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const useNumistaTypes = (options?: {
|
||||
filterBanknotes?: boolean;
|
||||
onSuccess?: (value?: NumistaType[] | null) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const searchParams = router.asPath.split("?")[1];
|
||||
const searchParams = router.asPath.split('?')[1];
|
||||
const { data: coins } = useQuery<NumistaType[]>({
|
||||
queryKey: ["types", searchParams],
|
||||
keepPreviousData: true,
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/types?" + searchParams ?? "");
|
||||
const res = await fetch(`/api/types?${searchParams}` ?? '');
|
||||
const types = await res.json();
|
||||
options?.onSuccess?.(types);
|
||||
|
||||
return types;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['types', searchParams],
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
if (options?.filterBanknotes) {
|
||||
return coins?.filter(({ category }) => category !== "banknote");
|
||||
return coins?.filter(({ category }) => category !== 'banknote');
|
||||
}
|
||||
|
||||
return coins;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
export function getFlagEmoji(countryCode?: string | null) {
|
||||
if (!countryCode?.length) {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (countryCode) {
|
||||
case "GB-ENG":
|
||||
return "🏴";
|
||||
case "SUHH":
|
||||
return "☭";
|
||||
case 'GB-ENG':
|
||||
return '🏴';
|
||||
|
||||
case 'SUHH':
|
||||
return '☭';
|
||||
|
||||
default:
|
||||
return String.fromCodePoint(
|
||||
...countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0)),
|
||||
.split('')
|
||||
.map(char => 127397 + char.charCodeAt(0))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NumistaType } from "@/types/numista";
|
||||
import { NumistaType } from '@/types/numista';
|
||||
|
||||
export const sortTypes = (types: NumistaType[]) =>
|
||||
types
|
||||
@@ -6,31 +6,34 @@ export const sortTypes = (types: NumistaType[]) =>
|
||||
if (a.value?.numeric_value && b.value?.numeric_value) {
|
||||
return a.value.numeric_value - b.value.numeric_value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.value?.currency?.full_name && b.value?.currency?.full_name) {
|
||||
return a.value.currency.full_name.localeCompare(
|
||||
b.value.currency.full_name,
|
||||
);
|
||||
return a.value.currency.full_name.localeCompare(b.value.currency.full_name);
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.issuer && b.issuer) {
|
||||
return a.issuer.name.localeCompare(b.issuer.name);
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.max_year && b.max_year) {
|
||||
return a.max_year - b.max_year;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.min_year && b.min_year) {
|
||||
return a.min_year - b.min_year;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/theme.css";
|
||||
import "primereact/resources/primereact.min.css";
|
||||
import "primeicons/primeicons.css";
|
||||
import type { AppContext, AppProps } from "next/app";
|
||||
import App from "next/app";
|
||||
import Div100vh from "react-div-100vh";
|
||||
import { PrimeReactProvider } from "primereact/api";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import '@/styles/globals.css';
|
||||
import '@/styles/theme.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
import { PrimeReactProvider } from 'primereact/api';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import App, { AppContext, AppProps } from 'next/app';
|
||||
import Div100vh from 'react-div-100vh';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -28,8 +27,9 @@ MyApp.getInitialProps = async (appContext: AppContext) => {
|
||||
const appProps = await App.getInitialProps(appContext);
|
||||
|
||||
if (appContext.ctx.res?.statusCode === 404) {
|
||||
appContext.ctx.res.writeHead(302, { Location: "/" });
|
||||
appContext.ctx.res.writeHead(302, { Location: '/' });
|
||||
appContext.ctx.res.end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
import { Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Html lang={'en'}>
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getCountries } from "@/api/countries/get-countries";
|
||||
import type { NextApiHandler } from "next";
|
||||
/* eslint-disable no-console */
|
||||
import { NextApiHandler } from 'next';
|
||||
import { getCountries } from '@/api/countries/get-countries';
|
||||
|
||||
const handler: NextApiHandler = async ({ query }, res) => {
|
||||
if (Array.isArray(query.filter)) {
|
||||
res.status(400).json({
|
||||
message: `Invalid filter ${JSON.stringify(query.filter)}`,
|
||||
message: `Invalid filter ${JSON.stringify(query.filter)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const countries = await getCountries(query.filter);
|
||||
|
||||
@@ -15,8 +18,8 @@ const handler: NextApiHandler = async ({ query }, res) => {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
message: `Something went wrong`,
|
||||
error: String(error),
|
||||
message: `Something went wrong`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getCountryCounts } from "@/api/countries/get-country-counts";
|
||||
import type { NextApiHandler } from "next";
|
||||
/* eslint-disable no-console */
|
||||
import { NextApiHandler } from 'next';
|
||||
import { getCountryCounts } from '@/api/countries/get-country-counts';
|
||||
|
||||
const handler: NextApiHandler = async (_req, res) => {
|
||||
try {
|
||||
@@ -9,8 +10,8 @@ const handler: NextApiHandler = async (_req, res) => {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
message: `Something went wrong`,
|
||||
error: String(error),
|
||||
message: `Something went wrong`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getCurrencies } from "@/api/currencies/get-currencies";
|
||||
import type { NextApiHandler } from "next";
|
||||
/* eslint-disable no-console */
|
||||
import { NextApiHandler } from 'next';
|
||||
import { getCurrencies } from '@/api/currencies/get-currencies';
|
||||
|
||||
const handler: NextApiHandler = async ({ query }, res) => {
|
||||
if (Array.isArray(query.filter)) {
|
||||
res.status(400).json({
|
||||
message: `Invalid filter ${JSON.stringify(query.filter)}`,
|
||||
message: `Invalid filter ${JSON.stringify(query.filter)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currencies = await getCurrencies(query.filter);
|
||||
|
||||
@@ -15,8 +18,8 @@ const handler: NextApiHandler = async ({ query }, res) => {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
message: `Something went wrong`,
|
||||
error: String(error),
|
||||
message: `Something went wrong`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import type { NextApiHandler } from "next";
|
||||
import { readFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { getType } from "@/api/types/get-type";
|
||||
/* eslint-disable no-console */
|
||||
import { NextApiHandler } from 'next';
|
||||
import { getType } from '@/api/types/get-type';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { spawn } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
const asyncSpawn = (command: string, args: string[]) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.stdout?.on("data", process.stdout.write);
|
||||
child.stderr?.on("data", process.stderr.write);
|
||||
child.on("close", (code) => {
|
||||
child.on('error', reject);
|
||||
child.stdout?.on('data', process.stdout.write);
|
||||
child.stderr?.on('data', process.stderr.write);
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
@@ -25,7 +26,7 @@ const asyncSpawn = (command: string, args: string[]) => {
|
||||
|
||||
const getPreviousResult = async (id: number) => {
|
||||
try {
|
||||
return await readFile(path.join("./public/photos/joined", id + ".jpg"));
|
||||
return await readFile(path.join('./public/photos/joined', `${id}.jpg`));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
@@ -45,78 +46,79 @@ const generateImage = async (id: number) => {
|
||||
}
|
||||
|
||||
if (coin.obverse?.picture) {
|
||||
await asyncSpawn("convert", [
|
||||
path.join("./public", coin.obverse.picture),
|
||||
"-resize",
|
||||
"256x256!",
|
||||
path.join("./public/photos/joined", id + "-obverse.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
path.join('./public', coin.obverse.picture),
|
||||
'-resize',
|
||||
'256x256!',
|
||||
path.join('./public/photos/joined', `${id}-obverse.jpg`)
|
||||
]);
|
||||
} else {
|
||||
await asyncSpawn("convert", [
|
||||
"-size",
|
||||
"512x64",
|
||||
"xc:" + coin.color,
|
||||
path.join("./public/photos/joined", id + "-edge.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
'-size',
|
||||
'512x64',
|
||||
`xc:${coin.color}`,
|
||||
path.join('./public/photos/joined', `${id}-edge.jpg`)
|
||||
]);
|
||||
}
|
||||
|
||||
if (coin.reverse?.picture) {
|
||||
await asyncSpawn("convert", [
|
||||
path.join("./public", coin.reverse.picture),
|
||||
"-resize",
|
||||
"256x256!",
|
||||
...(coin.orientation === "coin" ? ["-rotate", "180"] : []),
|
||||
path.join("./public/photos/joined", id + "-reverse.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
path.join('./public', coin.reverse.picture),
|
||||
'-resize',
|
||||
'256x256!',
|
||||
...(coin.orientation === 'coin' ? ['-rotate', '180'] : []),
|
||||
path.join('./public/photos/joined', `${id}-reverse.jpg`)
|
||||
]);
|
||||
} else {
|
||||
await asyncSpawn("convert", [
|
||||
"-size",
|
||||
"512x64",
|
||||
"xc:" + coin.color,
|
||||
path.join("./public/photos/joined", id + "-edge.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
'-size',
|
||||
'512x64',
|
||||
`xc:${coin.color}`,
|
||||
path.join('./public/photos/joined', `${id}-edge.jpg`)
|
||||
]);
|
||||
}
|
||||
|
||||
if (coin.edge?.picture) {
|
||||
await asyncSpawn("convert", [
|
||||
path.join("./public", coin.edge.picture),
|
||||
"-resize",
|
||||
"512x64!",
|
||||
path.join("./public/photos/joined", id + "-edge.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
path.join('./public', coin.edge.picture),
|
||||
'-resize',
|
||||
'512x64!',
|
||||
path.join('./public/photos/joined', `${id}-edge.jpg`)
|
||||
]);
|
||||
} else {
|
||||
await asyncSpawn("convert", [
|
||||
"-size",
|
||||
"512x64",
|
||||
"xc:" + coin.color,
|
||||
path.join("./public/photos/joined", id + "-edge.jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
'-size',
|
||||
'512x64',
|
||||
`xc:${coin.color}`,
|
||||
path.join('./public/photos/joined', `${id}-edge.jpg`)
|
||||
]);
|
||||
}
|
||||
|
||||
await asyncSpawn("convert", [
|
||||
path.join("./public/photos/joined", id + "-obverse.jpg"),
|
||||
path.join("./public/photos/joined", id + "-reverse.jpg"),
|
||||
"+append",
|
||||
path.join("./public/photos/joined", id + ".jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
path.join('./public/photos/joined', `${id}-obverse.jpg`),
|
||||
path.join('./public/photos/joined', `${id}-reverse.jpg`),
|
||||
'+append',
|
||||
path.join('./public/photos/joined', `${id}.jpg`)
|
||||
]);
|
||||
|
||||
for (let index = 0; index < 4; index++) {
|
||||
await asyncSpawn("convert", [
|
||||
path.join("./public/photos/joined", id + ".jpg"),
|
||||
path.join("./public/photos/joined", id + "-edge.jpg"),
|
||||
"-append",
|
||||
path.join("./public/photos/joined", id + ".jpg"),
|
||||
await asyncSpawn('convert', [
|
||||
path.join('./public/photos/joined', `${id}.jpg`),
|
||||
path.join('./public/photos/joined', `${id}-edge.jpg`),
|
||||
'-append',
|
||||
path.join('./public/photos/joined', `${id}.jpg`)
|
||||
]);
|
||||
}
|
||||
|
||||
return await readFile(path.join("./public/photos/joined", id + ".jpg"));
|
||||
return await readFile(path.join('./public/photos/joined', `${id}.jpg`));
|
||||
};
|
||||
|
||||
const handler: NextApiHandler = async ({ query }, res) => {
|
||||
if (!Number(query.id)) {
|
||||
res.status(400).json({
|
||||
message: `Invalid id ${JSON.stringify(query.id)}`,
|
||||
message: `Invalid id ${JSON.stringify(query.id)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,17 +127,19 @@ const handler: NextApiHandler = async ({ query }, res) => {
|
||||
|
||||
if (!image) {
|
||||
res.status(404).json({
|
||||
message: `Coin ${JSON.stringify(query.id)} not found`,
|
||||
message: `Coin ${JSON.stringify(query.id)} not found`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
res.setHeader("Content-Type", "image/jpg");
|
||||
|
||||
res.setHeader('Content-Type', 'image/jpg');
|
||||
res.status(200).send(image);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
message: `Something went wrong`,
|
||||
error: String(error),
|
||||
message: `Something went wrong`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,70 +1,61 @@
|
||||
import { getTypes } from "@/api/types/get-types";
|
||||
import type { NextApiHandler } from "next";
|
||||
/* eslint-disable no-console */
|
||||
import { NextApiHandler } from 'next';
|
||||
import { getTypes } from '@/api/types/get-types';
|
||||
|
||||
const handler: NextApiHandler = async ({ query }, res) => {
|
||||
if (Array.isArray(query.search)) {
|
||||
res.status(400).json({
|
||||
message: `Invalid search ${JSON.stringify(query.search)}`,
|
||||
message: `Invalid search ${JSON.stringify(query.search)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Array.isArray(query.faceValue) ||
|
||||
(query.faceValue && !Number(query.faceValue))
|
||||
) {
|
||||
|
||||
if (Array.isArray(query.faceValue) || (query.faceValue && !Number(query.faceValue))) {
|
||||
res.status(400).json({
|
||||
message: `Invalid faceValue ${JSON.stringify(query.faceValue)}`,
|
||||
message: `Invalid faceValue ${JSON.stringify(query.faceValue)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Array.isArray(query.category) ||
|
||||
(query.category && !["coins", "banknotes"].includes(query.category))
|
||||
) {
|
||||
|
||||
if (Array.isArray(query.category) || (query.category && !['coins', 'banknotes'].includes(query.category))) {
|
||||
res.status(400).json({
|
||||
message: `Invalid category ${JSON.stringify(query.category)}`,
|
||||
message: `Invalid category ${JSON.stringify(query.category)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Array.isArray(query.special) ||
|
||||
(query.special && !["yes", "no", "both"].includes(query.special))
|
||||
) {
|
||||
|
||||
if (Array.isArray(query.special) || (query.special && !['yes', 'no', 'both'].includes(query.special))) {
|
||||
res.status(400).json({
|
||||
message: `Invalid special filter ${JSON.stringify(query.special)}`,
|
||||
message: `Invalid special filter ${JSON.stringify(query.special)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Array.isArray(query.yearRange) &&
|
||||
(query.yearRange.length !== 2 ||
|
||||
query.yearRange.some((year) => !Number(year)))
|
||||
) {
|
||||
|
||||
if (Array.isArray(query.yearRange) && (query.yearRange.length !== 2 || query.yearRange.some(year => !Number(year)))) {
|
||||
res.status(400).json({
|
||||
message: `Invalid year range ${JSON.stringify(query.yearRange)}`,
|
||||
message: `Invalid year range ${JSON.stringify(query.yearRange)}`
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const coins = await getTypes({
|
||||
search: query.search,
|
||||
category: query.category as "coins" | "banknotes" | undefined,
|
||||
category: query.category as 'coins' | 'banknotes' | undefined,
|
||||
faceValue: query.faceValue ? Number(query.faceValue) : undefined,
|
||||
special: query.special as "yes" | "no" | "both" | undefined,
|
||||
yearRange: Array.isArray(query.yearRange)
|
||||
? (query.yearRange.map(Number) as [number, number])
|
||||
: undefined,
|
||||
search: query.search,
|
||||
special: query.special as 'yes' | 'no' | 'both' | undefined,
|
||||
yearRange: Array.isArray(query.yearRange) ? (query.yearRange.map(Number) as [number, number]) : undefined,
|
||||
...(query.currencies && {
|
||||
currencies: Array.isArray(query.currencies)
|
||||
? query.currencies
|
||||
: [query.currencies],
|
||||
currencies: Array.isArray(query.currencies) ? query.currencies : [query.currencies]
|
||||
}),
|
||||
...(query.countries && {
|
||||
countries: Array.isArray(query.countries)
|
||||
? query.countries
|
||||
: [query.countries],
|
||||
}),
|
||||
countries: Array.isArray(query.countries) ? query.countries : [query.countries]
|
||||
})
|
||||
});
|
||||
|
||||
if (!coins.length) {
|
||||
@@ -75,8 +66,8 @@ const handler: NextApiHandler = async ({ query }, res) => {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
message: `Something went wrong`,
|
||||
error: String(error),
|
||||
message: `Something went wrong`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Head from "next/head";
|
||||
import { GetStaticProps, NextPage } from "next";
|
||||
import { getCountryCounts } from "@/api/countries/get-country-counts";
|
||||
import Link from "next/link";
|
||||
import { getFlagEmoji } from "@/core/utils/flags";
|
||||
import { GetStaticProps, NextPage } from 'next';
|
||||
import { getCountryCounts } from '@/api/countries/get-country-counts';
|
||||
import { getFlagEmoji } from '@/core/utils/flags';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
|
||||
type Props = {
|
||||
countryCounts: Awaited<ReturnType<typeof getCountryCounts>>;
|
||||
@@ -12,47 +12,41 @@ const CountriesPage: NextPage<Props> = ({ countryCounts }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Coins</title>
|
||||
<meta name="description" content="My father's coin collection" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>{'Coins'}</title>
|
||||
<meta content={"My father's coin collection"} name={'description'} />
|
||||
<meta content={'width=device-width, initial-scale=1'} name={'viewport'} />
|
||||
<link href={'/favicon.ico'} rel={'icon'} />
|
||||
</Head>
|
||||
<main
|
||||
style={{
|
||||
backgroundColor: "#111",
|
||||
color: "white",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: '#111',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 32,
|
||||
padding: "32px 16px",
|
||||
padding: '32px 16px',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{countryCounts.map((country) => (
|
||||
<div key={country.code} style={{ maxWidth: 640, margin: "auto" }}>
|
||||
<b style={{ textAlign: "center", display: "block" }}>
|
||||
{getFlagEmoji(country.iso) + " " + country.name}
|
||||
</b>
|
||||
{countryCounts.map(country => (
|
||||
<div key={country.code} style={{ margin: 'auto', maxWidth: 640 }}>
|
||||
<b style={{ display: 'block', textAlign: 'center' }}>{`${getFlagEmoji(country.iso)} ${country.name}`}</b>
|
||||
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyItems: "center",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
alignItems: 'center',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
justifyItems: 'center',
|
||||
marginTop: 8,
|
||||
width: "min(640px, 95vw)",
|
||||
width: 'min(640px, 95vw)'
|
||||
}}
|
||||
>
|
||||
{country.coins?.count ? (
|
||||
<>
|
||||
<Link href={"/pile/?countries=" + country.name}>
|
||||
{"pile (" + country.coins.sum + ")"}
|
||||
</Link>
|
||||
<Link
|
||||
href={"/showcase/?category=coins&countries=" + country.name}
|
||||
>
|
||||
{"coins (" + country.coins.count + ")"}
|
||||
<Link href={`/pile/?countries=${country.name}`}>{`pile (${country.coins.sum})`}</Link>
|
||||
<Link href={`/showcase/?category=coins&countries=${country.name}`}>
|
||||
{`coins (${country.coins.count})`}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
@@ -63,12 +57,8 @@ const CountriesPage: NextPage<Props> = ({ countryCounts }) => {
|
||||
)}
|
||||
|
||||
{country.banknotes?.count ? (
|
||||
<Link
|
||||
href={
|
||||
"/showcase/?category=banknotes&countries=" + country.name
|
||||
}
|
||||
>
|
||||
{"banknotes (" + country.banknotes.count + ")"}
|
||||
<Link href={`/showcase/?category=banknotes&countries=${country.name}`}>
|
||||
{`banknotes (${country.banknotes.count})`}
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
@@ -86,8 +76,8 @@ export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
|
||||
return {
|
||||
props: {
|
||||
countryCounts,
|
||||
},
|
||||
countryCounts
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import Head from "next/head";
|
||||
import { Pile } from "@/components/pile";
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import { Header } from "@/components/header";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import styles from "@/styles/search.module.css";
|
||||
import { Filters } from "@/components/filters";
|
||||
import { useState } from "react";
|
||||
import { Filters } from '@/components/filters';
|
||||
import { GetServerSidePropsContext, GetServerSidePropsResult, NextPage } from 'next';
|
||||
import { Header } from '@/components/header';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import { Pile } from '@/components/pile';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import styles from '@/styles/search.module.css';
|
||||
|
||||
const PilePage: NextPage<{ query: ParsedUrlQuery }> = ({ query }) => {
|
||||
type Props = { query: ParsedUrlQuery };
|
||||
|
||||
const PilePage: NextPage<Props> = ({ query }) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Coin pile</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="My father's coin collection in a big pile"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>{'Coin pile'}</title>
|
||||
<meta content={"My father's coin collection in a big pile"} name={'description'} />
|
||||
<meta content={'width=device-width, initial-scale=1'} name={'viewport'} />
|
||||
<link href={'/favicon.ico'} rel={'icon'} />
|
||||
</Head>
|
||||
|
||||
<Header route={"pile"} onOpenDrawer={() => setDrawerVisible(true)} />
|
||||
<Header onOpenDrawer={() => setDrawerVisible(true)} route={'pile'} />
|
||||
|
||||
<Filters
|
||||
visible={drawerVisible}
|
||||
onHide={() => setDrawerVisible(false)}
|
||||
className={styles.sidebar}
|
||||
filterBanknotes
|
||||
onHide={() => setDrawerVisible(false)}
|
||||
query={query}
|
||||
visible={drawerVisible}
|
||||
/>
|
||||
|
||||
<Pile />
|
||||
@@ -37,10 +36,10 @@ const PilePage: NextPage<{ query: ParsedUrlQuery }> = ({ query }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => ({
|
||||
export const getServerSideProps = ({ query }: GetServerSidePropsContext): GetServerSidePropsResult<Props> => ({
|
||||
props: {
|
||||
query,
|
||||
},
|
||||
query
|
||||
}
|
||||
});
|
||||
|
||||
export default PilePage;
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import styles from "@/styles/search.module.css";
|
||||
import Head from "next/head";
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import { Showcase } from "@/components/showcase";
|
||||
import { Header } from "@/components/header";
|
||||
import { useState } from "react";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import { Filters } from "@/components/filters";
|
||||
import { Filters } from '@/components/filters';
|
||||
import { GetServerSidePropsContext, GetServerSidePropsResult, NextPage } from 'next';
|
||||
import { Header } from '@/components/header';
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import { Showcase } from '@/components/showcase';
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import styles from '@/styles/search.module.css';
|
||||
|
||||
const CoinsPage: NextPage<{ query: ParsedUrlQuery }> = ({ query }) => {
|
||||
type Props = { query: ParsedUrlQuery };
|
||||
|
||||
const ShowcasePage: NextPage<Props> = ({ query }) => {
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Coins</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="An exhibition of my father's coin collection"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<title>{'Showcase'}</title>
|
||||
<meta content={"An exhibition of my father's coin collection"} name={'description'} />
|
||||
<meta content={'width=device-width, initial-scale=1'} name={'viewport'} />
|
||||
<link href={'/favicon.ico'} rel={'icon'} />
|
||||
</Head>
|
||||
|
||||
<Header route={"showcase"} onOpenDrawer={() => setDrawerVisible(true)} />
|
||||
<Header onOpenDrawer={() => setDrawerVisible(true)} route={'showcase'} />
|
||||
|
||||
<Filters
|
||||
visible={drawerVisible}
|
||||
onHide={() => setDrawerVisible(false)}
|
||||
className={styles.sidebar}
|
||||
onHide={() => setDrawerVisible(false)}
|
||||
query={query}
|
||||
visible={drawerVisible}
|
||||
/>
|
||||
|
||||
<Showcase />
|
||||
@@ -36,10 +35,10 @@ const CoinsPage: NextPage<{ query: ParsedUrlQuery }> = ({ query }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ query }) => ({
|
||||
export const getServerSideProps = ({ query }: GetServerSidePropsContext): GetServerSidePropsResult<Props> => ({
|
||||
props: {
|
||||
query,
|
||||
},
|
||||
query
|
||||
}
|
||||
});
|
||||
|
||||
export default CoinsPage;
|
||||
export default ShowcasePage;
|
||||
|
||||
1543
src/styles/theme.css
1543
src/styles/theme.css
File diff suppressed because it is too large
Load Diff
@@ -1,101 +1,42 @@
|
||||
export type NumistaCategory = "coin" | "banknote" | "exonumia";
|
||||
export type NumistaCategory = 'coin' | 'banknote' | 'exonumia';
|
||||
|
||||
export interface NumistaType {
|
||||
/** @description Unique ID of the type on Numista */
|
||||
id: number;
|
||||
/** @description URL to the type on Numista */
|
||||
url?: string;
|
||||
/** @description Title of the type */
|
||||
title: string;
|
||||
/**
|
||||
* @description Category
|
||||
* @enum {string}
|
||||
*/
|
||||
category: NumistaCategory;
|
||||
issuer?: Issuer;
|
||||
/** @description First year the type was produced (in the Gregorian calendar). */
|
||||
min_year?: number;
|
||||
/** @description Last year the type was produced (in the Gregorian calendar). */
|
||||
max_year?: number;
|
||||
/** @description Type */
|
||||
type?: string;
|
||||
/** @description Face value */
|
||||
value?: {
|
||||
/** @description Face value in text format */
|
||||
text?: string;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Face value as a floating number.
|
||||
*/
|
||||
numeric_value?: number;
|
||||
/** @description If the value is better described as a fraction, this is the numerator of the fraction */
|
||||
numerator?: number;
|
||||
/** @description If the value is better described as a fraction, this is the denominator of the fraction */
|
||||
denominator?: number;
|
||||
currency?: Currency;
|
||||
};
|
||||
/** @description Ruling authorities (emperor, queen, period, etc.) */
|
||||
ruler?: {
|
||||
/** @description Unique ID of the ruling authority on Numista */
|
||||
id: number;
|
||||
/** @description Name of the ruling authority */
|
||||
name: string;
|
||||
/** @description Identifier of the ruling authority at Wikidata, starting with a "Q" */
|
||||
wikidata_id?: string;
|
||||
/** @description Dynasty, house, extended period, or any other group of ruling authorities */
|
||||
group?: {
|
||||
/** @description Unique ID of the ruling authority group on Numista */
|
||||
id: number;
|
||||
/** @description Name of the ruling authority group */
|
||||
name: string;
|
||||
};
|
||||
}[];
|
||||
/** @description Information about the demonetization of the coin or banknote */
|
||||
demonetization?: {
|
||||
/** @description True if the type is demonetized, false if it is not demonetized */
|
||||
is_demonetized: boolean;
|
||||
/**
|
||||
* Format: date
|
||||
* @description Date of demonetisation (YYYY-MM-DD)
|
||||
*/
|
||||
demonetization_date?: string;
|
||||
};
|
||||
/** @description Shape */
|
||||
shape?: string;
|
||||
/** @description Dominant color calculated from the pictures */
|
||||
color: string;
|
||||
/** @description For commemorated types, short description of the commemorated topic (event, person, etc.) */
|
||||
commemorated_topic?: string;
|
||||
/** @description General comments about the type (HTML format) */
|
||||
comments?: string;
|
||||
/** @description Composition (metallic content) */
|
||||
composition?: {
|
||||
/** @description Description of the composition */
|
||||
text?: string;
|
||||
};
|
||||
/** @description Manufacturing technique */
|
||||
technique?: {
|
||||
/** @description Description of the technique */
|
||||
text?: string;
|
||||
/** @description How many of this type are included in my personal collection */
|
||||
count?: number;
|
||||
/** @description Information about the demonetization of the coin or banknote */
|
||||
demonetization?: {
|
||||
/**
|
||||
* Format: date
|
||||
* @description Date of demonetisation (YYYY-MM-DD)
|
||||
*/
|
||||
demonetization_date?: string;
|
||||
/** @description True if the type is demonetized, false if it is not demonetized */
|
||||
is_demonetized: boolean;
|
||||
};
|
||||
/**
|
||||
* Format: float
|
||||
* @description Weight in grams
|
||||
*/
|
||||
weight?: number;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Size (diameter) in millimeters
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Thickness of the coin in millimeters
|
||||
*/
|
||||
thickness?: number;
|
||||
/**
|
||||
* @description Orientation of the coin ("coin", "medal", "three" (3 o'clock), "nine" (9 o'clock), or "variable")
|
||||
* @enum {string}
|
||||
*/
|
||||
orientation?: "coin" | "medal" | "variable" | "three" | "nine";
|
||||
obverse?: CoinSide;
|
||||
reverse?: CoinSide;
|
||||
edge?: CoinSide;
|
||||
watermark?: CoinSide;
|
||||
/** @description Unique ID of the type on Numista */
|
||||
id: number;
|
||||
issuer?: Issuer;
|
||||
/** @description Last year the type was produced (in the Gregorian calendar). */
|
||||
max_year?: number;
|
||||
/** @description First year the type was produced (in the Gregorian calendar). */
|
||||
min_year?: number;
|
||||
/** @description Mints where the coin was minted */
|
||||
mints?: {
|
||||
/** @description Unique ID of the mint on Numista */
|
||||
@@ -103,6 +44,12 @@ export interface NumistaType {
|
||||
/** @description Name of the mint */
|
||||
name: string;
|
||||
}[];
|
||||
obverse?: CoinSide;
|
||||
/**
|
||||
* @description Orientation of the coin ("coin", "medal", "three" (3 o'clock), "nine" (9 o'clock), or "variable")
|
||||
* @enum {string}
|
||||
*/
|
||||
orientation?: 'coin' | 'medal' | 'variable' | 'three' | 'nine';
|
||||
/** @description Printers where the banknote was printed */
|
||||
printers?: {
|
||||
/** @description Unique ID of the printer on Numista */
|
||||
@@ -110,76 +57,129 @@ export interface NumistaType {
|
||||
/** @description Name of the printer */
|
||||
name: string;
|
||||
}[];
|
||||
/** @description For types which are part of a series, the name of the series */
|
||||
series?: string;
|
||||
/** @description For commemorated types, short description of the commemorated topic (event, person, etc.) */
|
||||
commemorated_topic?: string;
|
||||
/** @description General comments about the type (HTML format) */
|
||||
comments?: string;
|
||||
/** @description References of the type in other catalogues */
|
||||
references?: Reference[];
|
||||
/** @description List of related types */
|
||||
related_types?: {
|
||||
/** @description Unique ID of the type on Numista */
|
||||
id: number;
|
||||
/** @description Title of the type */
|
||||
title: string;
|
||||
/**
|
||||
* @description Category
|
||||
* @enum {string}
|
||||
*/
|
||||
category?: "coin" | "banknote" | "exonumia";
|
||||
category?: 'coin' | 'banknote' | 'exonumia';
|
||||
/** @description Unique ID of the type on Numista */
|
||||
id: number;
|
||||
issuer?: Issuer;
|
||||
/** @description First year the type was producted (in the Gregorian calendar). */
|
||||
min_year?: number;
|
||||
/** @description Last year the type was producted (in the Gregorian calendar). */
|
||||
max_year?: number;
|
||||
/** @description First year the type was producted (in the Gregorian calendar). */
|
||||
min_year?: number;
|
||||
/** @description Title of the type */
|
||||
title: string;
|
||||
}[];
|
||||
reverse?: CoinSide;
|
||||
/** @description Ruling authorities (emperor, queen, period, etc.) */
|
||||
ruler?: {
|
||||
/** @description Dynasty, house, extended period, or any other group of ruling authorities */
|
||||
group?: {
|
||||
/** @description Unique ID of the ruling authority group on Numista */
|
||||
id: number;
|
||||
/** @description Name of the ruling authority group */
|
||||
name: string;
|
||||
};
|
||||
/** @description Unique ID of the ruling authority on Numista */
|
||||
id: number;
|
||||
/** @description Name of the ruling authority */
|
||||
name: string;
|
||||
/** @description Identifier of the ruling authority at Wikidata, starting with a "Q" */
|
||||
wikidata_id?: string;
|
||||
}[];
|
||||
/** @description For types which are part of a series, the name of the series */
|
||||
series?: string;
|
||||
/** @description Shape */
|
||||
shape?: string;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Size (diameter) in millimeters
|
||||
*/
|
||||
size?: number;
|
||||
/** @description List of tags */
|
||||
tags?: string[];
|
||||
/** @description References of the type in other catalogues */
|
||||
references?: Reference[];
|
||||
/** @description How many of this type are included in my personal collection */
|
||||
count?: number;
|
||||
/** @description Dominant color calculated from the pictures */
|
||||
color: string;
|
||||
/** @description Manufacturing technique */
|
||||
technique?: {
|
||||
/** @description Description of the technique */
|
||||
text?: string;
|
||||
};
|
||||
/**
|
||||
* Format: float
|
||||
* @description Thickness of the coin in millimeters
|
||||
*/
|
||||
thickness?: number;
|
||||
/** @description Title of the type */
|
||||
title: string;
|
||||
/** @description Type */
|
||||
type?: string;
|
||||
/** @description URL to the type on Numista */
|
||||
url?: string;
|
||||
/** @description Face value */
|
||||
value?: {
|
||||
currency?: Currency;
|
||||
/** @description If the value is better described as a fraction, this is the denominator of the fraction */
|
||||
denominator?: number;
|
||||
/** @description If the value is better described as a fraction, this is the numerator of the fraction */
|
||||
numerator?: number;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Face value as a floating number.
|
||||
*/
|
||||
numeric_value?: number;
|
||||
/** @description Face value in text format */
|
||||
text?: string;
|
||||
};
|
||||
watermark?: CoinSide;
|
||||
/**
|
||||
* Format: float
|
||||
* @description Weight in grams
|
||||
*/
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
interface Issuer {
|
||||
/** @description ISO 3166-1 code of the issuer, sometimes -2 or -3 */
|
||||
iso: string;
|
||||
/** @description Unique ID of the issuer on Numista */
|
||||
code: string;
|
||||
/** @description ISO 3166-1 code of the issuer, sometimes -2 or -3 */
|
||||
iso: string;
|
||||
/** @description Name of the issuer */
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Currency {
|
||||
/** @description Full name of the currency, including dates */
|
||||
full_name: string;
|
||||
/** @description Unique ID of the currency on Numista */
|
||||
id: number;
|
||||
/** @description Name of the currency */
|
||||
name: string;
|
||||
/** @description Full name of the currency, including dates */
|
||||
full_name: string;
|
||||
}
|
||||
|
||||
interface Reference {
|
||||
/** @description The catalogue in which the reference can be found */
|
||||
catalogue: {
|
||||
/** @description ID of the catalogue in Numista */
|
||||
id: number;
|
||||
/** @description Code identifying the catalogue */
|
||||
code: string;
|
||||
/** @description ID of the catalogue in Numista */
|
||||
id: number;
|
||||
};
|
||||
/** @description Number of the coin in the catalogue */
|
||||
number: string;
|
||||
}
|
||||
|
||||
export interface CoinSide {
|
||||
/** @description Name of the engraver(s) */
|
||||
engravers?: string[];
|
||||
/** @description Name of the designer(s) */
|
||||
designers?: string[];
|
||||
/** @description Description of the side of the coin */
|
||||
description?: string;
|
||||
/** @description Name of the designer(s) */
|
||||
designers?: string[];
|
||||
/** @description Name of the engraver(s) */
|
||||
engravers?: string[];
|
||||
/** @description Lettering visible on the side of the coin */
|
||||
lettering?: string;
|
||||
/** @description Scripts used to write the lettering on the side of the coins */
|
||||
@@ -187,14 +187,10 @@ export interface CoinSide {
|
||||
/** @description Name of the script */
|
||||
name: string;
|
||||
}[];
|
||||
/** @description Legend visible on the side of the coin with abbreviations replaced by full words */
|
||||
unabridged_legend?: string;
|
||||
/** @description Translation of the lettering visible on the side of the coin */
|
||||
lettering_translation?: string;
|
||||
/** @description URL to the picture of the side of the coin */
|
||||
picture?: string;
|
||||
/** @description URL to the thumbnail of the picture of the side of the coin */
|
||||
thumbnail?: string;
|
||||
/** @description Name of the owner of the picture. Pictures should not be used without consent from their owner. */
|
||||
picture_copyright?: string;
|
||||
/** @description URL to the website of the owner of the picture. Pictures should not be used without consent from their owner. */
|
||||
@@ -203,4 +199,8 @@ export interface CoinSide {
|
||||
picture_license_name?: string;
|
||||
/** @description URL to the license of the picture, if the owner of the picture specified a license. */
|
||||
picture_license_url?: string;
|
||||
/** @description URL to the thumbnail of the picture of the side of the coin */
|
||||
thumbnail?: string;
|
||||
/** @description Legend visible on the side of the coin with abbreviations replaced by full words */
|
||||
unabridged_legend?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user