-
-
diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro
new file mode 100644
index 0000000..b02acd5
--- /dev/null
+++ b/src/pages/[locale]/index.astro
@@ -0,0 +1,88 @@
+---
+import { Icon } from "astro-icon/components";
+import AppLayout from "layouts/AppLayout.astro";
+---
+
+
+
+
+
+
+
+
Accord’s Library
+
Discover • Analyze • Translate • Archive
+
+
+
+
+ Accord’s Library aims at gathering and archiving all of Yoko Taro’s
+ work. Yoko Taro is a Japanese video game director and scenario writer.
+ He is best-known for his work on the NieR and Drakengard (Drag-on
+ Dragoon) franchises. To complement his games, Yoko Taro likes to publish
+ side materials in the form of books, novellas, artbooks, stage plays,
+ manga, drama CDs, and comics. Those side materials can be very difficult
+ to find. His work goes all the way back to 2003, and most of them are
+ out of print after having been released solely in Japan, sometimes in
+ limited quantities. Their prices on the second hand market have
+ skyrocketed, ranging all the way to hundreds if not thousand of dollars
+ for the rarest items.
+
+ This is where this library takes its meaning, in trying to help the
+ community grow by providing translators, writers, and wiki’s
+ contributors a simple way to access these records filled with stories,
+ artworks, and knowledge.
+
+ We are a small group of Yoko Taro’s fans that decided to join forces and
+ create a website and a community. Our motto is Discover • Analyze • Translate • Archive (D.A.T.A. for short). We started with the goal of gathering and
+ archiving as much side-materials/merch as possible. But since then, our
+ ambition grew and we decided to create a full-fledged website that will
+ also include news articles, lore, summaries, translations, and
+ transcriptions. Hopefully one day, we will be up there in the list of
+ notable resources for Drakengard and NieR fans.
+
+ A comprehensive list of all Yokoverse’s side materials (books, novellas,
+ artbooks, stage plays, manga, drama CDs, and comics). For each, we provide
+ photos, scans, and transcript of the content, information about what it is,
+ when and how it was released, size, initial price…
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
deleted file mode 100644
index b71e709..0000000
--- a/src/styles/global.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.theme-color-light {
- --theme-color-highlight: 255 241 224;
- --theme-color-light: 255 237 216;
- --theme-color-mid: 240 209 179;
- --theme-color-dark: 156 102 68;
- --theme-color-shade: 192 132 94;
- --theme-color-black: 27 24 17;
-}
-
-.theme-color-dark {
- --theme-color-highlight: 44 40 37;
- --theme-color-light: 38 34 30;
- --theme-color-mid: 57 45 34;
- --theme-color-dark: 192 132 94;
- --theme-color-shade: 25 25 20;
- --theme-color-black: 235 234 231;
-}
diff --git a/src/styles/reset.css b/src/styles/reset.css
deleted file mode 100644
index 1d9cc6a..0000000
--- a/src/styles/reset.css
+++ /dev/null
@@ -1,8 +0,0 @@
-:where(button) {
- background-color: inherit;
- color: inherit;
- border: initial;
- padding: initial;
- margin: initial;
- cursor: pointer;
-}
diff --git a/src/env.d.ts b/src/typings/env.d.ts
similarity index 87%
rename from src/env.d.ts
rename to src/typings/env.d.ts
index 14a4904..c46707b 100644
--- a/src/env.d.ts
+++ b/src/typings/env.d.ts
@@ -1,4 +1,4 @@
-///
+///
///
interface ImportMetaEnv {
diff --git a/src/typings/html-attributes.d.ts b/src/typings/html-attributes.d.ts
new file mode 100644
index 0000000..03ca06a
--- /dev/null
+++ b/src/typings/html-attributes.d.ts
@@ -0,0 +1,4 @@
+declare namespace astroHTML.JSX {
+ interface HTMLAttributes {}
+}
+
diff --git a/src/typings/turbo.d.ts b/src/typings/turbo.d.ts
new file mode 100644
index 0000000..a127271
--- /dev/null
+++ b/src/typings/turbo.d.ts
@@ -0,0 +1,6 @@
+// https://turbo.hotwired.dev/reference/drive
+declare namespace Turbo {
+ const cache = {
+ clear: () => null,
+ };
+}
diff --git a/src/utils/Elementos.ts b/src/utils/Elementos.ts
new file mode 100644
index 0000000..0cf95f2
--- /dev/null
+++ b/src/utils/Elementos.ts
@@ -0,0 +1,26 @@
+import type { Observable } from "./micro-observables";
+
+export class Elementos {
+ readonly element: HTMLElement;
+ constructor(readonly selector: string) {
+ this.element = document.querySelector(selector)!;
+ }
+
+ onClick(listener: () => void) {
+ this.element.addEventListener("click", listener);
+ }
+
+ setClass(
+ className: string | { ifTrue: string; ifFalse: string },
+ observable: Observable
+ ) {
+ observable.subscribe((val) => {
+ if (typeof className === "string") {
+ this.element.classList.toggle(className, val);
+ } else {
+ this.element.classList.toggle(className.ifFalse, val === false);
+ this.element.classList.toggle(className.ifTrue, val === true);
+ }
+ });
+ }
+}
diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts
new file mode 100644
index 0000000..0af06cd
--- /dev/null
+++ b/src/utils/cookies.ts
@@ -0,0 +1,4 @@
+export enum CookieNames {
+ MENU_PANEL_REDUCED = "menuPanelReduced",
+ THEME_COLOR = "themeColor",
+}
diff --git a/src/utils/micro-observables/baseObservable.ts b/src/utils/micro-observables/baseObservable.ts
new file mode 100644
index 0000000..eceb845
--- /dev/null
+++ b/src/utils/micro-observables/baseObservable.ts
@@ -0,0 +1,226 @@
+import { batchedUpdater } from "./batchedUpdater";
+import { type Plugin } from "./plugin";
+import { PluginManager } from "./pluginManager";
+
+const UNSET = Symbol();
+
+const plugins = new PluginManager();
+
+const capturedInputFrames: BaseObservable[][] = [];
+let shouldCaptureNextInput = false;
+
+let batchedObservables: BaseObservable[] = [];
+let batchDepth = 0;
+
+export type Listener = (val: T, prevVal: T) => void;
+export type Unsubscriber = () => void;
+export type Options = { [key: string]: any };
+
+export class BaseObservable {
+ private _val: T;
+ private _prevVal: T | typeof UNSET = UNSET;
+ private _options: Options;
+ private _inputs: BaseObservable[] = [];
+ private _outputs: BaseObservable[] = [];
+ private _listeners: Listener[] = [];
+ private _attachedToInputs = false;
+
+ constructor(val: T, options: Options = {}) {
+ this._val = val;
+ this._options = options;
+ plugins.onCreate(this, val);
+ }
+
+ get(): T {
+ const capturedInputs = capturedInputFrames[capturedInputFrames.length - 1];
+ if (capturedInputs && shouldCaptureNextInput) {
+ try {
+ shouldCaptureNextInput = false;
+ capturedInputs.push(this);
+ return this._get();
+ } finally {
+ shouldCaptureNextInput = true;
+ }
+ } else {
+ return this._get();
+ }
+ }
+
+ protected _get(): T {
+ const shouldEvaluate = !this._attachedToInputs || this._prevVal !== UNSET;
+ return shouldEvaluate ? this._evaluate() : this._val;
+ }
+
+ protected _evaluate(): T {
+ return this._val;
+ }
+
+ protected _set(val: T) {
+ if (this._val !== val) {
+ this._addToBatchRecursively();
+ this._val = val;
+ }
+ }
+
+ subscribe(listener: Listener): Unsubscriber {
+ this._listeners.push(listener);
+ this._attachToInputs();
+
+ let listenerRemoved = false;
+ return () => {
+ if (!listenerRemoved) {
+ listenerRemoved = true;
+ this._listeners.splice(this._listeners.indexOf(listener), 1);
+ this._detachFromInputs();
+ }
+ };
+ }
+
+ /**
+ * @deprecated Use observable.subscribe() instead
+ */
+ onChange = this.subscribe;
+
+ protected onBecomeObserved() {
+ // Called when the first listener subscribes to the observable or to one of its outputs
+ }
+
+ protected onBecomeUnobserved() {
+ // Called when the last listener unsubscribes from the observable and from all of its outputs
+ }
+
+ getInputs(): BaseObservable[] {
+ return this._inputs;
+ }
+
+ getOptions(): O {
+ return this._options as O;
+ }
+
+ withOptions(options: Partial): this {
+ this._options = { ...this._options, ...options };
+ return this;
+ }
+
+ protected static _captureInputs(block: () => T): BaseObservable[] {
+ try {
+ const capturedInputs: any[] = [];
+ capturedInputFrames.push(capturedInputs);
+ shouldCaptureNextInput = true;
+ block();
+ return capturedInputs;
+ } finally {
+ capturedInputFrames.pop();
+ shouldCaptureNextInput = false;
+ }
+ }
+
+ protected _addInput(input: BaseObservable) {
+ this._inputs.push(input);
+ if (this._attachedToInputs) {
+ this._attachToInput(input);
+ }
+ }
+
+ protected _removeInput(input: BaseObservable) {
+ this._inputs.splice(this._inputs.indexOf(input), 1);
+ if (this._attachedToInputs) {
+ this._detachFromInput(input);
+ }
+ }
+
+ private _shouldAttachToInputs(): boolean {
+ // Only attach to inputs when at least one listener is subscribed to the observable or to one of its outputs.
+ // This is done to avoid unused observables being references by their inputs, preventing garbage-collection.
+ return this._listeners.length > 0 || this._outputs.length > 0;
+ }
+
+ private _attachToInputs() {
+ if (!this._attachedToInputs && this._shouldAttachToInputs()) {
+ this._attachedToInputs = true;
+
+ // Since the observable was not attached to its inputs, its value may be outdated.
+ // Refresh it so that listeners will be called with the correct prevValue the next time an input changes.
+ this._val = this._evaluate();
+
+ this.onBecomeObserved();
+ plugins.onBecomeObserved(this);
+
+ for (const input of this._inputs) {
+ this._attachToInput(input);
+ input._attachToInputs();
+ }
+ }
+ }
+
+ private _detachFromInputs() {
+ if (this._attachedToInputs && !this._shouldAttachToInputs()) {
+ this._attachedToInputs = false;
+ for (const input of this._inputs) {
+ this._detachFromInput(input);
+ input._detachFromInputs();
+ }
+
+ this.onBecomeUnobserved();
+ plugins.onBecomeUnobserved(this);
+ }
+ }
+
+ private _attachToInput(input: BaseObservable) {
+ input._outputs.push(this);
+ plugins.onAttach(this, input);
+ }
+
+ private _detachFromInput(input: BaseObservable) {
+ input._outputs.splice(input._outputs.indexOf(this), 1);
+ plugins.onDetach(this, input);
+ }
+
+ private _addToBatchRecursively() {
+ if (this._prevVal === UNSET) {
+ this._prevVal = this._val;
+
+ // Add the observable and its outputs in reverse topological order
+ for (const output of this._outputs) {
+ output._addToBatchRecursively();
+ }
+ batchedObservables.push(this);
+ }
+ }
+
+ protected static _batch(block: () => void) {
+ try {
+ batchDepth++;
+ if (batchDepth === 1 && batchedUpdater) {
+ batchedUpdater(block);
+ } else {
+ block();
+ }
+ } finally {
+ batchDepth--;
+ if (batchDepth === 0) {
+ const observablesToUpdate = batchedObservables;
+ batchedObservables = [];
+
+ // Iterate in reverse order as _addToBatchRecursively() adds them in reverse topological order
+ observablesToUpdate.reverse().forEach((observable) => {
+ const prevVal = observable._prevVal;
+ observable._prevVal = UNSET;
+ observable._val = observable._evaluate();
+ const val = observable._val;
+
+ if (val !== prevVal) {
+ for (const listener of observable._listeners.slice()) {
+ listener(val, prevVal);
+ }
+ plugins.onChange(observable, val, prevVal);
+ }
+ });
+ }
+ }
+ }
+
+ protected static _use(plugin: Plugin) {
+ plugins.use(plugin);
+ }
+}
diff --git a/src/utils/micro-observables/batchedUpdater.ts b/src/utils/micro-observables/batchedUpdater.ts
new file mode 100644
index 0000000..f80e07b
--- /dev/null
+++ b/src/utils/micro-observables/batchedUpdater.ts
@@ -0,0 +1,7 @@
+export type BatchedUpdater = (block: () => void) => void;
+
+export let batchedUpdater: BatchedUpdater | undefined;
+
+export function setBatchedUpdater(updater: BatchedUpdater | undefined) {
+ batchedUpdater = updater;
+}
diff --git a/src/utils/micro-observables/index.ts b/src/utils/micro-observables/index.ts
new file mode 100644
index 0000000..ccf5ab6
--- /dev/null
+++ b/src/utils/micro-observables/index.ts
@@ -0,0 +1,5 @@
+export * from "./baseObservable";
+export * from "./batchedUpdater";
+export * from "./observable";
+export * from "./plugin";
+export * from "./withPersistence";
diff --git a/src/utils/micro-observables/memoize.ts b/src/utils/micro-observables/memoize.ts
new file mode 100644
index 0000000..6ce51ab
--- /dev/null
+++ b/src/utils/micro-observables/memoize.ts
@@ -0,0 +1,25 @@
+export function memoize(func: (args: T) => U): (args: T) => U {
+ let lastArgs: T | undefined;
+ let lastResult!: U;
+
+ return (args: T) => {
+ let argsHaveChanged = false;
+ if (!lastArgs || args.length !== lastArgs.length) {
+ argsHaveChanged = true;
+ } else {
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] !== lastArgs[i]) {
+ argsHaveChanged = true;
+ break;
+ }
+ }
+ }
+
+ if (argsHaveChanged) {
+ lastArgs = args;
+ lastResult = func(args);
+ }
+
+ return lastResult;
+ };
+}
diff --git a/src/utils/micro-observables/observable.ts b/src/utils/micro-observables/observable.ts
new file mode 100644
index 0000000..cd16814
--- /dev/null
+++ b/src/utils/micro-observables/observable.ts
@@ -0,0 +1,211 @@
+import { BaseObservable, type Options } from "./baseObservable";
+import { memoize } from "./memoize";
+import { type Plugin } from "./plugin";
+
+export type ObservableValue = T extends Observable ? U : never;
+export type ObservableValues = { [K in keyof T]: ObservableValue };
+
+export function observable(
+ val: T | Observable,
+ options?: Options
+): WritableObservable {
+ return new WritableObservable(val, options);
+}
+
+export function derived(derive: () => T): Observable {
+ return Observable.compute(derive);
+}
+
+export class Observable extends BaseObservable {
+ protected _valInput: Observable | undefined;
+
+ constructor(val: T | Observable, options?: Options) {
+ super(val instanceof Observable ? val.get() : val, options);
+ this._updateValInput(val);
+ }
+
+ protected override _evaluate(): T {
+ return this._valInput ? this._valInput.get() : super._evaluate();
+ }
+
+ select(selector: (val: T) => U | Observable): Observable {
+ return new DerivedObservable([this], ([val]) => selector(val as T));
+ }
+
+ /**
+ * @deprecated Use observable.select() instead
+ */
+ transform = this.select;
+
+ onlyIf(predicate: (val: T) => boolean): Observable {
+ let filteredVal: T | undefined = undefined;
+ return this.select((val) => {
+ if (predicate(val)) {
+ filteredVal = val;
+ }
+ return filteredVal;
+ });
+ }
+
+ default(
+ defaultVal: NonNullable | Observable>
+ ): Observable> {
+ return this.select((val) => val ?? defaultVal);
+ }
+
+ as(): Observable {
+ return this as unknown as Observable;
+ }
+
+ static select[], U>(
+ observables: [...T],
+ selector: (...vals: ObservableValues) => U
+ ): Observable {
+ return new DerivedObservable(observables, (vals) => selector(...vals));
+ }
+
+ /**
+ * @deprecated Use Observable.select() instead
+ */
+ static from[]>(
+ ...observables: T
+ ): Observable> {
+ return new DerivedObservable(observables, (values) => values);
+ }
+
+ static merge(observables: Observable[]): Observable {
+ return new DerivedObservable(observables, (values) => values);
+ }
+
+ static latest[]>(
+ ...observables: T
+ ): Observable> {
+ let prevValues: T[] | undefined;
+ return new DerivedObservable(observables, (values) => {
+ const val = !prevValues
+ ? values[0]
+ : values.find((it, index) => it !== prevValues![index])!;
+ prevValues = values;
+ return val;
+ });
+ }
+
+ static compute(compute: () => U): Observable {
+ return new ComputedObservable(compute);
+ }
+
+ static fromPromise(
+ promise: Promise,
+ onError?: (error: any) => E
+ ): Observable {
+ const obs = observable(undefined);
+ promise.then(
+ (val) => obs.set(val),
+ (e) => onError && obs.set(onError(e))
+ );
+ return obs;
+ }
+
+ toPromise(): Promise {
+ return new Promise((resolve) => {
+ const unsubscribe = this.subscribe((val) => {
+ resolve(val);
+ unsubscribe();
+ });
+ });
+ }
+
+ static batch(block: () => void) {
+ BaseObservable._batch(block);
+ }
+
+ static use(plugin: Plugin) {
+ BaseObservable._use(plugin);
+ }
+
+ protected _updateValInput(val: T | Observable) {
+ if (this._valInput !== val) {
+ if (this._valInput) {
+ this._removeInput(this._valInput);
+ this._valInput = undefined;
+ }
+ if (val instanceof Observable) {
+ this._addInput(val);
+ this._valInput = val;
+ }
+ }
+ }
+}
+
+export class WritableObservable extends Observable {
+ set(val: T | Observable) {
+ this._updateValInput(val);
+ Observable.batch(() =>
+ this._set(val instanceof Observable ? val.get() : val)
+ );
+ }
+
+ update(updater: (val: T) => T | Observable) {
+ this.set(updater(this.get()));
+ }
+
+ readOnly(): Observable {
+ return this;
+ }
+}
+
+class DerivedObservable[]> extends Observable {
+ private _compute: (vals: ObservableValues) => T | Observable;
+ private _computeInputs: U;
+
+ constructor(
+ computeInputs: U,
+ compute: (vals: ObservableValues) => T | Observable
+ ) {
+ // No need to initialize it as it will be evaluated the first time get() or subscribe() is called
+ super(undefined as any);
+ this._compute = memoize(compute);
+ this._computeInputs = computeInputs;
+ for (const input of computeInputs) {
+ this._addInput(input);
+ }
+ }
+
+ override _evaluate(): T {
+ const computed = this._compute(
+ this._computeInputs.map((input) => input.get()) as ObservableValues
+ );
+ this._updateValInput(computed);
+ return computed instanceof Observable ? computed.get() : computed;
+ }
+}
+
+class ComputedObservable extends Observable {
+ private _compute: () => T;
+ private _currentInputs = new Set>();
+
+ constructor(compute: () => T) {
+ // No need to initialize it as it will be evaluated the first time get() or subscribe() is called
+ super(undefined as any);
+ this._compute = compute;
+ }
+
+ override _evaluate(): T {
+ let value!: T;
+
+ const inputs = new Set(
+ BaseObservable._captureInputs(() => (value = this._compute()))
+ );
+ inputs.forEach((input) => {
+ if (!this._currentInputs.has(input)) {
+ this._addInput(input);
+ } else {
+ this._currentInputs.delete(input);
+ }
+ });
+ this._currentInputs.forEach((input) => this._removeInput(input));
+ this._currentInputs = inputs;
+
+ return value;
+ }
+}
diff --git a/src/utils/micro-observables/plugin.ts b/src/utils/micro-observables/plugin.ts
new file mode 100644
index 0000000..81f58da
--- /dev/null
+++ b/src/utils/micro-observables/plugin.ts
@@ -0,0 +1,10 @@
+import { BaseObservable } from "./baseObservable";
+
+export interface Plugin {
+ onCreate?(observable: BaseObservable, val: any): void;
+ onChange?(observable: BaseObservable, val: any, prevVal: any): void;
+ onBecomeObserved?(observable: BaseObservable): void;
+ onBecomeUnobserved?(observable: BaseObservable): void;
+ onAttach?(observable: BaseObservable, input: BaseObservable): void;
+ onDetach?(observable: BaseObservable, input: BaseObservable): void;
+}
diff --git a/src/utils/micro-observables/pluginManager.ts b/src/utils/micro-observables/pluginManager.ts
new file mode 100644
index 0000000..a1e6339
--- /dev/null
+++ b/src/utils/micro-observables/pluginManager.ts
@@ -0,0 +1,34 @@
+import { BaseObservable } from "./baseObservable";
+import { type Plugin } from "./plugin";
+
+export class PluginManager {
+ private _plugins: Plugin[] = [];
+
+ use(plugin: Plugin) {
+ this._plugins.push(plugin);
+ }
+
+ onCreate(observable: BaseObservable, val: any) {
+ this._plugins.forEach((it) => it.onCreate?.(observable, val));
+ }
+
+ onChange(observable: BaseObservable, val: any, prevVal: any) {
+ this._plugins.forEach((it) => it.onChange?.(observable, val, prevVal));
+ }
+
+ onBecomeObserved(observable: BaseObservable) {
+ this._plugins.forEach((it) => it.onBecomeObserved?.(observable));
+ }
+
+ onBecomeUnobserved(observable: BaseObservable) {
+ this._plugins.forEach((it) => it.onBecomeUnobserved?.(observable));
+ }
+
+ onAttach(observable: BaseObservable, input: BaseObservable) {
+ this._plugins.forEach((it) => it.onAttach?.(observable, input));
+ }
+
+ onDetach(observable: BaseObservable, input: BaseObservable) {
+ this._plugins.forEach((it) => it.onDetach?.(observable, input));
+ }
+}
diff --git a/src/utils/micro-observables/withPersistence.ts b/src/utils/micro-observables/withPersistence.ts
new file mode 100644
index 0000000..124625a
--- /dev/null
+++ b/src/utils/micro-observables/withPersistence.ts
@@ -0,0 +1,12 @@
+import { WritableObservable, observable } from ".";
+import Cookies from "js-cookie";
+
+export const observableWithPersistence = (
+ cookieKey: string,
+ defaultValue: T
+): WritableObservable => {
+ const valueFromCookie = Cookies.get(cookieKey) as T | undefined;
+ const obs = observable(valueFromCookie ?? defaultValue);
+ obs.subscribe((val) => Cookies.set(cookieKey, val as string));
+ return obs;
+};
diff --git a/src/utils/turbo.ts b/src/utils/turbo.ts
new file mode 100644
index 0000000..709e863
--- /dev/null
+++ b/src/utils/turbo.ts
@@ -0,0 +1,2 @@
+export const onLoad = (callback: () => void) =>
+ document.documentElement.addEventListener("turbo:load", callback);
diff --git a/src/utils/urls.ts b/src/utils/urls.ts
new file mode 100644
index 0000000..23ad996
--- /dev/null
+++ b/src/utils/urls.ts
@@ -0,0 +1,2 @@
+export const getLocalizedUrl = (url: string, locale: string = "en"): string =>
+ `/${locale}${url}`;
diff --git a/tsconfig.json b/tsconfig.json
index 2f9a22c..0c37159 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,9 +2,6 @@
"extends": "astro/tsconfigs/strictest",
"compilerOptions": {
"types": ["bun-types"],
- "baseUrl": "./",
- "paths": {
- "src/*": ["./src/*"]
- }
+ "baseUrl": "./src",
}
}