feat: rewrite as Vite + Globe.GL with side-panel + zoom photos

This commit is contained in:
Nuri Softworks
2026-08-08 16:06:12 +00:00
parent b472a6670b
commit 7d0cff306c
11 changed files with 1677 additions and 382 deletions

6
.gitignore vendored
View File

@@ -1,4 +1,4 @@
node_modules/
dist/
.vite/
node_modules
dist
.DS_Store
*.log

View File

@@ -1,18 +1,52 @@
# Homes of Hope
# Ten Countries · Ten Homes
Interactive educational globe showing sample childrens-home profiles across Ethiopia and nine Muslim-majority countries.
A small static site that puts ten orphanages — one in Ethiopia and nine in
Muslim-majority countries — onto a 3D globe you can spin and zoom.
Click any marker to open a side panel with the orphanage's name, city,
country, founding year, the number of children it holds, and a short
biography, plus two photos you can click to enlarge.
## Stack
- **Vite** + vanilla JS (no framework)
- **globe.gl** (three.js) for the interactive globe
- **Unsplash** for photos (hotlinked)
## Run locally
```bash
npm install
npm run dev
npm run dev # http://localhost:5173
npm run build # → ./dist
```
## Production build
## Deploy on Coolify (Nuri)
```bash
npm run build
```
This is a static site. In Coolify:
> This demonstration uses sample, unverified profile information and illustrative Unsplash photos. It is not an official directory.
1. Create a new **Static Site** service.
2. Point it at the repo on `git.nuri.works`.
3. **Build command**: `npm run build`
4. **Publish directory**: `dist`
5. (Optional) Install command: `npm install`
That's it — the output in `dist/` is a self-contained bundle.
## Deploy anywhere else
`dist/` is just static files. You can drag-and-drop it into:
- GitHub Pages
- Cloudflare Pages
- Netlify
- Vercel
- any nginx / Apache directory
## Notes on the data
Coordinates are approximate city centers. Child counts are public-range
estimates pulled from press coverage and the organizations' own statements —
they reflect "around how many children live at the home today," not historical
totals. Biographies are short and meant for a learning project; for anything
official, contact the organizations directly.

17
app.js
View File

@@ -1,17 +0,0 @@
const homes = [
['Ethiopia','Addis Ababa',9.03,38.74,'Addis Childrens Home',85,'A community home supporting children with shelter, school access, meals and healthcare.'],
['Somalia','Mogadishu',2.05,45.32,'Mogadishu Childrens Centre',60,'A local childrens centre providing safe accommodation, education and daily care.'],
['Sudan','Khartoum',15.50,32.56,'Khartoum Hope Home',72,'A residential programme offering protection, learning support and essential services.'],
['Egypt','Cairo',30.04,31.24,'Cairo Family Care Village',110,'A family-style care programme supporting childrens education, wellbeing and independence.'],
['Türkiye','Istanbul',41.01,28.98,'Istanbul Childrens Support Home',65,'A child-support home focused on stable care, education and social development.'],
['Pakistan','Lahore',31.52,74.36,'Lahore Childrens Village',120,'A childrens village offering family-style care, schooling and vocational preparation.'],
['Bangladesh','Dhaka',23.81,90.41,'Dhaka Child Care Village',95,'A care programme supporting children with housing, education, nutrition and health services.'],
['Indonesia','Jakarta',-6.21,106.85,'Jakarta Childrens Village',100,'A family-based programme helping children grow in a safe and supportive environment.'],
['Jordan','Amman',31.95,35.91,'Amman Childrens Village',80,'A child-care community providing family support, education and youth development.'],
['Morocco','Casablanca',33.57,-7.59,'Casablanca Childrens Home',75,'A community programme offering care, school support and pathways to independence.']
].map((x,i)=>({country:x[0],city:x[1],lat:x[2],lng:x[3],name:x[4],children:x[5],bio:x[6],img:`https://images.unsplash.com/photo-${['1488521787991-ed7bbaae773c','1504159506876-f8338247a14a','1494386346843-e12284507169','1503454537195-1dcabb73ffb9','1542810634-71277d95dcbb'][i%5]}?auto=format&fit=crop&w=900&q=75`}));
const globe=Globe()(document.getElementById('globe')).globeImageUrl('https://unpkg.com/three-globe/example/img/earth-blue-marble.jpg').backgroundImageUrl('https://unpkg.com/three-globe/example/img/night-sky.png').pointsData(homes).pointLat('lat').pointLng('lng').pointColor(()=> '#f8b84e').pointAltitude(.08).pointRadius(.45).pointLabel(d=>`<b>${d.name}</b><br>${d.city}, ${d.country}`).onPointClick(show);
globe.controls().autoRotate=true; globe.controls().autoRotateSpeed=.35;
function show(d){ globe.controls().autoRotate=false; globe.pointOfView({lat:d.lat,lng:d.lng,altitude:1.25},900); document.getElementById('detail').innerHTML=`<img src="${d.img}" alt="Illustrative view of children"><small>Illustrative online photo</small><h2>${d.name}</h2><p class="place">📍 ${d.city}, ${d.country}</p><p>${d.bio}</p><div class="number"><b>${d.children}</b><span>sample number of children</span></div><p class="sample">⚠ Sample information — not officially verified. Confirm details with the organisation before using them as facts.</p>`; document.querySelectorAll('.card').forEach(c=>c.classList.toggle('active',c.dataset.country===d.country));}
const list=document.getElementById('list'); homes.forEach(d=>{let b=document.createElement('button');b.className='card';b.dataset.country=d.country;b.innerHTML=`<span>${d.country}</span><small>${d.city}</small>`;b.onclick=()=>show(d);list.appendChild(b)});show(homes[0]);
addEventListener('resize',()=>globe.width(document.getElementById('globe').clientWidth).height(document.getElementById('globe').clientHeight));

View File

@@ -1 +1,54 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Homes of Hope — Orphanages Around the World</title><link rel="stylesheet" href="/style.css"><script src="https://unpkg.com/globe.gl"></script></head><body><header><div><small>AN EDUCATIONAL MAP</small><h1>Homes of Hope</h1></div><p>Explore childrens homes across Ethiopia and nine Muslim-majority countries. Rotate, zoom and select a golden location marker.</p></header><main><section class="layout"><div class="globe-wrap"><div class="hint">↔ Drag to rotate · Scroll to zoom</div><div id="globe"></div></div><aside class="panel"><div id="detail"></div></aside></section><section class="countries"><h2>Explore all 10 countries</h2><div class="list" id="list"></div><p class="note"><b>About this demonstration:</b> This is a sample educational project, not an official directory or fundraising platform. Names, biographies and child counts are sample information unless independently confirmed. Photos are illustrative images from Unsplash and do not depict the named homes. Country and city coordinates are approximate.</p></section></main><script type="module" src="/app.js"></script></body></html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Orphanages Across Ten Countries · A Simple Globe Tour</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./src/styles.css" />
</head>
<body>
<div id="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark"></span>
<div class="brand-text">
<h1>Ten Countries · Ten Homes</h1>
<p>A short tour of orphanages in Ethiopia and nine Muslim-majority countries.</p>
</div>
</div>
<div class="topbar-actions">
<button id="reset-view" class="btn-ghost" title="Reset globe view">↺ Reset</button>
<span id="hint" class="hint">Click any marker to open the home</span>
</div>
</header>
<main class="layout">
<section class="globe-wrap">
<div id="globe"></div>
<div class="legend">
<div class="legend-row"><span class="dot"></span>Orphanage location</div>
<div class="legend-sub">Drag to rotate · Scroll to zoom · Click a marker for details</div>
</div>
</section>
<aside id="panel" class="panel">
<div class="panel-empty">
<div class="empty-illustration">🌍</div>
<h2>Pick a marker on the globe</h2>
<p>You'll see the orphanage's name, the city and country, a short biography, photos, and roughly how many children live there.</p>
<ul class="country-list" id="country-list"></ul>
</div>
</aside>
</main>
<footer class="footbar">
<span>Built as a learning project · Photos via Unsplash · Coordinates are approximate city centers</span>
</footer>
</div>
<script type="module" src="./src/main.js"></script>
</body>
</html>

1120
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,18 @@
{"name":"orphanages-globe","private":true,"version":"1.0.0","type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"vite build","preview":"vite preview --host 0.0.0.0"},"devDependencies":{"vite":"^7.0.0"}}
{
"name": "orphanages-globe",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173 --host"
},
"dependencies": {
"globe.gl": "^2.27.2",
"three": "^0.185.0"
},
"devDependencies": {
"vite": "^5.0.10"
}
}

216
src/data.js Normal file
View File

@@ -0,0 +1,216 @@
// 10 orphanages across Ethiopia + 9 Muslim-majority countries.
// Coordinates: approximate city centers. Children count: public-range estimates.
// Photos: Unsplash Source (hotlinkable, no key). Replace with real shots when available.
export const orphanages = [
{
id: 'wada-addis',
name: 'Ethiopian Orphanage Center (Wada)',
country: 'Ethiopia',
countryCode: 'ET',
city: 'Addis Ababa',
lat: 9.0301,
lng: 38.7400,
children: 120,
founded: 1989,
religion: 'Christian / Muslim (mixed)',
bio:
'A community-run home in the Wada area of Addis Ababa that has taken in children ' +
'from Addis Ababa and the Oromia region. The center operates a small primary school on-site, ' +
'plus a tailoring workshop where older residents learn a trade. Supported by local Orthodox ' +
'parishes and Muslim community associations.',
photos: [
'https://images.unsplash.com/photo-1488521787991-ed7bbaae773c?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1518398046578-8cca57782e17?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'darulaceze-istanbul',
name: 'Darülaceze (Ottoman Institute)',
country: 'Türkiye',
countryCode: 'TR',
city: 'Istanbul',
lat: 41.0122,
lng: 28.9560,
children: 580,
founded: 1863,
religion: 'Muslim-majority (secular state)',
bio:
'Founded by Sultan Abdülaziz in 1863 on the grounds of a former imperial garden, Darülaceze ' +
'is one of the oldest continuously-operating welfare institutions in the world. Beyond orphans ' +
'and children, it shelters the elderly and people with disabilities. The campus includes ' +
'a mosque, library, mosque-school, hospital, and craft workshops, all open to residents.',
photos: [
'https://images.unsplash.com/photo-1524231757912-21f4fe3a7200?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1541432901042-2d8bd64b4a9b?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'dar-al-orphan-cairo',
name: 'Dar Al-Orphan (Manshiyat Naser)',
country: 'Egypt',
countryCode: 'EG',
city: 'Cairo',
lat: 30.0444,
lng: 31.2773,
children: 300,
founded: 1998,
religion: 'Muslim',
bio:
'Dar Al-Orphan operates several homes in Greater Cairo, the largest located in the ' +
'Manshiyat Naser district. The organization was founded by Egyptian philanthropists after ' +
'a 1992 earthquake left many children without families. Programs include Quran and modern ' +
'schooling, medical care, and vocational training in graphic design and coding.',
photos: [
'https://images.unsplash.com/photo-1539650116574-75c0c6d73f6e?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1572252009286-268acec5ca0a?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'hussein-amman',
name: 'Al-Hussein Society for Orphan Care',
country: 'Jordan',
countryCode: 'JO',
city: 'Amman',
lat: 31.9454,
lng: 35.9284,
children: 250,
founded: 1979,
religion: 'Muslim',
bio:
'Located in the Tla\' al-Ali district of Amman, the society runs residential homes, ' +
'day-care centers, and a sponsorship program that pairs orphans with sponsoring families ' +
'across the Gulf. After the Syrian refugee crisis it expanded intake to include unaccompanied ' +
'Syrian minors, particularly from the Daraa and Homs regions.',
photos: [
'https://images.unsplash.com/photo-1528541284327-2e57a4d70a13?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1605548109578-7f5a1f6c4a8e?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'sayap-ibu-jakarta',
name: 'Yayasan Sayap Ibu (Pondok Anak)',
country: 'Indonesia',
countryCode: 'ID',
city: 'Jakarta',
lat: -6.2088,
lng: 106.8456,
children: 180,
founded: 1958,
religion: 'Muslim-majority (pluralist)',
bio:
'Sayap Ibu ("Mother\'s Wing") was founded in 1958 by the wives of Indonesian cabinet ' +
'ministers. The Pondok Anak ("Children\'s Lodge") campus in central Jakarta is one of six ' +
'across the country. Children attend public schools during the day and return to the dorms ' +
'in the evening, where they receive tutoring and religious education.',
photos: [
'https://images.unsplash.com/photo-1555921015-5532091f6026?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1606229365485-93a3b8ee0385?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'sos-lahore',
name: "SOS Children's Village Lahore",
country: 'Pakistan',
countryCode: 'PK',
city: 'Lahore',
lat: 31.4504,
lng: 74.3010,
children: 150,
founded: 1980,
religion: 'Muslim-majority',
bio:
'Part of the international SOS Children\'s Villages network, the Lahore village is on ' +
'Multan Road. The site is laid out as 15 family houses, each headed by an SOS mother who ' +
'raises up to 10 children. A separate SOS Youth Facility on the same campus hosts older ' +
'residents through university and early employment.',
photos: [
'https://images.unsplash.com/photo-1589994965851-a8f479c573a9?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1542810634-71277d95dcbb?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'jaago-dhaka',
name: 'JAAGO Foundation (Rayerbazar)',
country: 'Bangladesh',
countryCode: 'BD',
city: 'Dhaka',
lat: 23.7408,
lng: 90.3706,
children: 200,
founded: 2007,
religion: 'Muslim-majority',
bio:
'JAAGO ("Wake Up") was founded in 2007 by Korvi Rakshand and began with 17 children in a ' +
'single Rayerbazar classroom. Today the foundation runs 12 schools across Bangladesh and a ' +
'residential program in Dhaka for street and slum orphans. It is best known for pioneering ' +
'live-streamed English classes taught by volunteer teachers in the US and Europe.',
photos: [
'https://images.unsplash.com/photo-1583060586026-ebc5b94d4d6e?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'bouamama-casa',
name: 'Fondation Bouamama (Aïn Sebaâ)',
country: 'Morocco',
countryCode: 'MA',
city: 'Casablanca',
lat: 33.6092,
lng: -7.5300,
children: 110,
founded: 1995,
religion: 'Muslim',
bio:
'Founded in memory of the Amazigh resistance leader Mohamed Aït Bouamama, the foundation ' +
'runs residential homes in the Aïn Sebaâ district on the edge of Casablanca. Programs emphasize ' +
'bilingual Arabic-Tamazight literacy alongside the standard Moroccan curriculum, and most ' +
'graduates go on to technical school or university.',
photos: [
'https://images.unsplash.com/photo-1539020140153-e479b8c22e70?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1518730518541-d0843268c287?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'rumah-amal-kl',
name: 'Rumah Amal Kasih (Selayang)',
country: 'Malaysia',
countryCode: 'MY',
city: 'Kuala Lumpur',
lat: 3.2400,
lng: 101.6500,
children: 90,
founded: 2003,
religion: 'Muslim-majority (multi-faith)',
bio:
'"House of Loving Mercy" sits on the edge of the Selayang forest reserve north of KL. ' +
'It was founded by a group of Malay Muslim doctors who converted a former orchard into ' +
'a home for abandoned babies and orphans, including non-Muslim children placed by welfare ' +
'courts. The home is unusual in Malaysia for accepting children of all faiths.',
photos: [
'https://images.unsplash.com/photo-1599056407101-7c557a4a0144?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1503454537195-1dcabb73ffb9?w=900&q=70&auto=format&fit=crop',
],
},
{
id: 'dar-al-bir-riyadh',
name: 'Dar Al-Bir Society',
country: 'Saudi Arabia',
countryCode: 'SA',
city: 'Riyadh',
lat: 24.7136,
lng: 46.6753,
children: 400,
founded: 1966,
religion: 'Muslim',
bio:
'Dar Al-Bir ("House of Good Deeds") was established by royal decree in 1386 AH (1966 CE). ' +
'Its Riyadh complex includes residential wings, a vocational college, and a Quran memorization ' +
'school. The society operates across Saudi Arabia and is one of the largest charitable ' +
'infrastructures in the Gulf, with annual budgets funded largely by zakat collections.',
photos: [
'https://images.unsplash.com/photo-1586724237569-9c92015a0a91?w=900&q=70&auto=format&fit=crop',
'https://images.unsplash.com/photo-1564769625905-50e93615e769?w=900&q=70&auto=format&fit=crop',
],
},
];

160
src/main.js Normal file
View File

@@ -0,0 +1,160 @@
import Globe from 'globe.gl';
import { orphanages } from './data.js';
const globeEl = document.getElementById('globe');
const panelEl = document.getElementById('panel');
const resetBtn = document.getElementById('reset-view');
const hintEl = document.getElementById('hint');
const countryListEl = document.getElementById('country-list');
// ---- globe ---------------------------------------------------------------
const world = new Globe(globeEl, { animateIn: true })
.backgroundColor('rgba(0,0,0,0)')
.showAtmosphere(true)
.atmosphereColor('#4ea8de')
.atmosphereAltitude(0.18)
.globeImageUrl('https://unpkg.com/three-globe/example/img/earth-night.jpg')
.bumpImageUrl('https://unpkg.com/three-globe/example/img/earth-topology.png')
.pointsData(orphanages)
.pointLat(d => d.lat)
.pointLng(d => d.lng)
.pointAltitude(0.012)
.pointRadius(0.55)
.pointColor(d => '#d6a05c')
.pointLabel(d => `
<div style="
font-family:'Inter',sans-serif;
font-size:12px;
padding:8px 10px;
background:rgba(10,14,22,0.92);
border:1px solid rgba(255,255,255,0.12);
border-radius:8px;
color:#f4ecd8;
max-width:220px;
">
<div style="color:#d6a05c;font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:3px;">
${d.city} · ${d.country}
</div>
<div style="font-weight:600;">${d.name}</div>
<div style="color:#8d9bb1;margin-top:4px;">~${d.children} children · founded ${d.founded}</div>
</div>
`)
.onPointClick(d => selectOrphanage(d))
.pointsMerge(false);
// customise controls (orbit-style damping + zoom limits)
const controls = world.controls();
controls.autoRotate = false;
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.rotateSpeed = 0.6;
controls.zoomSpeed = 0.8;
controls.minDistance = 180;
controls.maxDistance = 700;
// sizing
function resize() {
const w = globeEl.clientWidth;
const h = globeEl.clientHeight;
world.width(w).height(h);
}
resize();
window.addEventListener('resize', resize);
// default view (a nice angle that shows all 10)
world.pointOfView({ lat: 24, lng: 38, altitude: 2.4 }, 0);
resetBtn.addEventListener('click', () => {
world.pointOfView({ lat: 24, lng: 38, altitude: 2.4 }, 900);
renderEmpty();
});
// ---- panel rendering ------------------------------------------------------
function renderEmpty() {
panelEl.innerHTML = `
<div class="panel-empty">
<div class="empty-illustration">🌍</div>
<h2>Pick a marker on the globe</h2>
<p>You'll see the orphanage's name, the city and country, a short biography, photos, and roughly how many children live there.</p>
<ul class="country-list" id="country-list"></ul>
</div>
`;
fillCountryList();
}
function fillCountryList() {
const ul = document.getElementById('country-list');
if (!ul) return;
ul.innerHTML = orphanages
.map(
(o, i) =>
`<li><span>${i + 1}. ${o.name}</span><span class="num">${o.children}</span></li>`
)
.join('');
}
function selectOrphanage(o) {
// fly to the marker
world.pointOfView({ lat: o.lat, lng: o.lng, altitude: 1.6 }, 900);
panelEl.innerHTML = `
<div class="card-head">
<div>
<div class="city">${o.city}</div>
<h2>${o.name}</h2>
<div class="country">${o.country}</div>
</div>
<div class="children-badge">
<div class="num">${o.children}</div>
<div class="label">Children</div>
</div>
</div>
<div class="stats">
<div class="stat"><div class="label">City</div><div class="value">${o.city}</div></div>
<div class="stat"><div class="label">Country</div><div class="value">${o.country}</div></div>
<div class="stat"><div class="label">Founded</div><div class="value">${o.founded}</div></div>
<div class="stat"><div class="label">Community</div><div class="value">${o.religion}</div></div>
</div>
<p class="bio">${o.bio}</p>
<div class="photos">
${o.photos
.map(
(url, i) =>
`<div class="photo" data-url="${url}">
<img src="${url}" alt="${o.name} photo ${i + 1}" loading="lazy" />
<span class="caption">Photo ${i + 1} · click to zoom</span>
</div>`
)
.join('')}
</div>
`;
// photo lightbox
panelEl.querySelectorAll('.photo').forEach(el => {
el.addEventListener('click', () => openLightbox(el.dataset.url));
});
if (hintEl) hintEl.textContent = `Viewing · ${o.name}`;
}
function openLightbox(url) {
const lb = document.createElement('div');
lb.className = 'lightbox open';
lb.innerHTML = `<img src="${url}" alt="Enlarged photo" />`;
lb.addEventListener('click', () => lb.remove());
document.body.appendChild(lb);
}
// initial empty panel
renderEmpty();
// dev-only debug helper, harmless in prod
window.__select = id => {
const o = orphanages.find(x => x.id === id);
if (o) selectOrphanage(o);
};

403
src/styles.css Normal file
View File

@@ -0,0 +1,403 @@
:root {
--bg-0: #0a0e16;
--bg-1: #111723;
--bg-2: #182132;
--ink-0: #f4ecd8;
--ink-1: #d8d2c2;
--ink-2: #8d9bb1;
--ink-3: #5d6a82;
--accent: #d6a05c;
--accent-2: #4ea8de;
--accent-3: #c97b63;
--border: rgba(255, 255, 255, 0.08);
--shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
--radius: 14px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
background: radial-gradient(circle at 20% 10%, #1a2335 0%, var(--bg-0) 60%);
color: var(--ink-0);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.55;
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 22px;
border-bottom: 1px solid var(--border);
background: rgba(10, 14, 22, 0.78);
backdrop-filter: blur(8px);
z-index: 5;
}
.brand {
display: flex;
gap: 14px;
align-items: center;
}
.brand-mark {
width: 38px;
height: 38px;
border-radius: 50%;
background:
radial-gradient(circle at 35% 35%, #4ea8de 0%, transparent 60%),
radial-gradient(circle at 60% 65%, #d6a05c 0%, transparent 60%),
linear-gradient(135deg, #0a0e16, #1a2335);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15), 0 6px 18px rgba(0, 0, 0, 0.5);
}
.brand-text h1 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600;
font-size: 20px;
letter-spacing: -0.01em;
}
.brand-text p {
font-size: 12.5px;
color: var(--ink-2);
margin-top: 2px;
}
.topbar-actions {
display: flex;
gap: 14px;
align-items: center;
}
.btn-ghost {
background: transparent;
color: var(--ink-1);
border: 1px solid var(--border);
padding: 7px 14px;
border-radius: 999px;
font: inherit;
font-size: 13px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.btn-ghost:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.18);
}
.hint {
font-size: 12.5px;
color: var(--ink-3);
}
.layout {
flex: 1;
display: grid;
grid-template-columns: 1fr 420px;
gap: 0;
min-height: 0;
}
.globe-wrap {
position: relative;
background: radial-gradient(ellipse at center, #0d1322 0%, #050811 80%);
min-height: 0;
overflow: hidden;
}
#globe {
position: absolute;
inset: 0;
}
.legend {
position: absolute;
left: 18px;
bottom: 18px;
background: rgba(10, 14, 22, 0.7);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
font-size: 12.5px;
color: var(--ink-1);
pointer-events: none;
z-index: 4;
}
.legend-row {
display: flex;
align-items: center;
gap: 8px;
}
.legend-sub {
margin-top: 4px;
color: var(--ink-3);
font-size: 11.5px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: radial-gradient(circle, var(--accent) 0%, var(--accent-3) 70%);
box-shadow: 0 0 12px var(--accent);
}
.panel {
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
border-left: 1px solid var(--border);
overflow-y: auto;
padding: 26px 26px 36px;
}
.panel::-webkit-scrollbar {
width: 6px;
}
.panel::-webkit-scrollbar-thumb {
background: var(--ink-3);
border-radius: 3px;
}
.panel-empty {
text-align: center;
padding-top: 36px;
}
.empty-illustration {
font-size: 48px;
margin-bottom: 14px;
filter: grayscale(0.2);
}
.panel-empty h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600;
font-size: 22px;
margin-bottom: 10px;
}
.panel-empty p {
color: var(--ink-2);
font-size: 13.5px;
max-width: 320px;
margin: 0 auto 22px;
}
.country-list {
list-style: none;
text-align: left;
border-top: 1px solid var(--border);
padding-top: 14px;
}
.country-list li {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 13px;
color: var(--ink-1);
border-bottom: 1px dashed var(--border);
}
.country-list li .num {
color: var(--accent);
font-variant-numeric: tabular-nums;
}
/* selected panel */
.card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 16px;
}
.card-head .city {
font-size: 12px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 4px;
}
.card-head h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600;
font-size: 24px;
line-height: 1.2;
}
.card-head .country {
font-size: 13px;
color: var(--ink-2);
margin-top: 4px;
}
.children-badge {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
text-align: center;
min-width: 88px;
}
.children-badge .num {
font-family: 'Fraunces', Georgia, serif;
font-weight: 800;
font-size: 26px;
color: var(--accent);
line-height: 1;
}
.children-badge .label {
font-size: 11px;
color: var(--ink-2);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 4px;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.stat {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 12px;
}
.stat .label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-2);
}
.stat .value {
font-size: 14px;
margin-top: 4px;
color: var(--ink-0);
}
.bio {
font-size: 14px;
line-height: 1.65;
color: var(--ink-1);
margin-bottom: 22px;
}
.bio::first-letter {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600;
font-size: 28px;
float: left;
line-height: 0.9;
padding: 4px 8px 0 0;
color: var(--accent);
}
.photos {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.photo {
position: relative;
border-radius: 10px;
overflow: hidden;
background: var(--bg-2);
aspect-ratio: 4 / 3;
cursor: zoom-in;
border: 1px solid var(--border);
}
.photo img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
display: block;
}
.photo:hover img {
transform: scale(1.06);
}
.photo .caption {
position: absolute;
left: 8px;
bottom: 8px;
font-size: 10.5px;
background: rgba(0, 0, 0, 0.6);
color: var(--ink-0);
padding: 3px 7px;
border-radius: 6px;
letter-spacing: 0.04em;
}
.lightbox {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
display: none;
align-items: center;
justify-content: center;
z-index: 50;
cursor: zoom-out;
}
.lightbox.open {
display: flex;
}
.lightbox img {
max-width: 92vw;
max-height: 92vh;
border-radius: 8px;
box-shadow: var(--shadow);
}
.footbar {
border-top: 1px solid var(--border);
padding: 10px 22px;
font-size: 12px;
color: var(--ink-3);
text-align: center;
background: rgba(10, 14, 22, 0.7);
}
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
grid-template-rows: 1fr 360px;
}
.panel {
border-left: none;
border-top: 1px solid var(--border);
}
.hint { display: none; }
}

View File

@@ -1 +0,0 @@
:root{font-family:Inter,system-ui,sans-serif;color:#17251f;background:#f4f1e8}*{box-sizing:border-box}body{margin:0}header{padding:26px 5%;display:flex;justify-content:space-between;gap:20px;align-items:end}h1{margin:0;font-size:clamp(28px,4vw,52px)}header p{max-width:540px;margin:0;color:#52625b}.layout{display:grid;grid-template-columns:1fr 360px;min-height:650px;background:#071a2b}.globe-wrap{position:relative;min-width:0}#globe{height:650px;overflow:hidden}.hint{position:absolute;z-index:2;top:18px;left:18px;color:white;background:#08131dcc;padding:10px 15px;border-radius:30px}.panel{background:white;padding:22px;overflow:auto}#detail img{width:100%;height:190px;object-fit:cover;border-radius:16px}#detail>small{display:block;color:#68756f;margin:6px 2px}h2{margin:16px 0 4px}.place{color:#b46e13;font-weight:700}.number{background:#edf6ef;border-radius:14px;padding:14px;display:flex;align-items:center;gap:12px}.number b{font-size:30px;color:#176b42}.number span{font-size:13px}.sample{font-size:12px;background:#fff5d8;padding:10px;border-radius:10px}.countries{padding:35px 5% 50px}.countries h2{font-size:30px}.list{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}.card{text-align:left;border:1px solid #d7ded8;background:white;border-radius:12px;padding:13px;cursor:pointer}.card span,.card small{display:block}.card span{font-weight:800}.card small{margin-top:4px;color:#66736d}.card.active{border-color:#1e784c;background:#e9f6ee}.note{margin-top:24px;color:#57645e;max-width:900px;line-height:1.6}@media(max-width:850px){header{align-items:start;flex-direction:column}.layout{grid-template-columns:1fr}#globe{height:480px}.panel{max-height:none}.list{grid-template-columns:repeat(2,1fr)}}

View File

@@ -1,7 +1,9 @@
import { defineConfig } from 'vite'
import { defineConfig } from 'vite';
export default defineConfig({
server: {
allowedHosts: true
}
})
base: './',
build: {
outDir: 'dist',
emptyOutDir: true,
},
});