Add movement gallery wings with period interiors and expand timeline features.

Movement galleries split large catalogs into chronological wings (~55 works), use era-themed 3D interiors with side-wall windows, wing navigator on the back exit, and front archways between wings. Also adds painting annotations, timeline event guides, portrait hover highlights, and documentation/API updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-21 16:54:19 +03:00
co-authored by Cursor
parent 0972b5df99
commit df29848d89
121 changed files with 4765 additions and 396 deletions
+48 -2
View File
@@ -12,7 +12,7 @@ const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.use(express.json({ limit: '20mb' }));
app.use('/images', express.static(IMAGE_DIR));
const INFLUENCE_LINKS_EXISTS = `
@@ -92,6 +92,44 @@ app.get('/api/timeline', async (req, res) => {
}
});
// Movement gallery — all paintings by artists in the movement, chronological
app.get('/api/movements/:id/gallery', async (req, res) => {
try {
const { id } = req.params;
const movement = await pool.query(
`SELECT m.*, e.name AS era_name
FROM art_movements m
LEFT JOIN historical_eras e ON m.era_id = e.id
WHERE m.id = $1`,
[id]
);
if (movement.rows.length === 0) {
return res.status(404).json({ error: 'Movement not found' });
}
const paintings = await pool.query(
`SELECT p.*,
a.name AS artist_name,
p.checkup_checked,
p.checkup_fixed,
(${INFLUENCE_LINKS_EXISTS}) AS has_influence_links
FROM paintings p
INNER JOIN artists a ON p.artist_id = a.id
WHERE a.movement_id = $1
ORDER BY p.year NULLS LAST, p.sort_order, a.birth_year NULLS LAST, p.title`,
[id]
);
res.json({
movement: movement.rows[0],
paintings: paintings.rows,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch movement gallery' });
}
});
// Artists for a movement in a time range
app.get('/api/movements/:id/artists', async (req, res) => {
try {
@@ -570,7 +608,7 @@ app.get('/api/paintings/:id', async (req, res) => {
return res.status(404).json({ error: 'Painting not found' });
}
const [influencedBy, influenced] = await Promise.all([
const [influencedBy, influenced, annotations] = await Promise.all([
pool.query(INFLUENCED_BY_SQL, [id]),
pool.query(
`SELECT * FROM (
@@ -593,12 +631,20 @@ app.get('/api/paintings/:id', async (req, res) => {
ORDER BY year NULLS LAST`,
[id]
),
pool.query(
`SELECT id, label, body, category, pos_x, pos_y, source_author, source, source_url, sort_order, confidence
FROM painting_annotations
WHERE painting_id = $1
ORDER BY sort_order, id`,
[id]
),
]);
res.json({
painting: painting.rows[0],
influencedBy: influencedBy.rows,
influenced: influenced.rows,
annotations: annotations.rows,
});
} catch (err) {
console.error(err);