Add untile eslint and prettier config

This commit is contained in:
2023-10-05 01:28:42 +01:00
parent 8abe9fb5e0
commit bb1b2f6677
51 changed files with 1453 additions and 2331 deletions

View File

@@ -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'
}
};

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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();

View File

@@ -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);

View File

@@ -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
})
);
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -2,12 +2,12 @@
const nextConfig = {
redirects: () => [
{
destination: "/",
destination: '/',
permanent: true,
source: "/showcase-banknotes",
},
source: '/showcase-banknotes'
}
],
reactStrictMode: true,
reactStrictMode: true
};
export default nextConfig;

View File

@@ -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"
}

View File

@@ -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 }
}
})
});
};

View File

@@ -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
}
})
};
});

View File

@@ -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);
};

View File

@@ -1,3 +1,3 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

View File

@@ -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;

View File

@@ -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[];

View File

@@ -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
}
}
};

View File

@@ -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"
/>
)}
</>

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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}
/>
</>

View File

@@ -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}
/>
</>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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>
</>
);

View File

@@ -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');

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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: []
}));

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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))
);
}
}

View File

@@ -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;
});

View File

@@ -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;
}

View File

@@ -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 />

View File

@@ -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`
});
}
};

View File

@@ -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`
});
}
};

View File

@@ -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`
});
}
};

View File

@@ -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`
});
}
};

View File

@@ -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`
});
}
};

View File

@@ -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
}
};
};

View File

@@ -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;

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}