Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/advanced/AdvancedTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,22 @@ export function AdvancedTools() {
<ul className="text-sm text-cyan-800 space-y-1">
<li>
<a href="/learn/origin/overview" className="underline hover:text-cyan-600">
Origin determination overview →
Origin determination overview <span aria-hidden="true">→</span>
</a>
</li>
<li>
<a href="/learn/origin/burma/ruby" className="underline hover:text-cyan-600">
Burmese ruby characteristics →
Burmese ruby characteristics <span aria-hidden="true">→</span>
</a>
</li>
<li>
<a href="/learn/origin/kashmir" className="underline hover:text-cyan-600">
Kashmir sapphire identification →
Kashmir sapphire identification <span aria-hidden="true">→</span>
</a>
</li>
<li>
<a href="/learn/origin/colombia" className="underline hover:text-cyan-600">
Colombian emerald features →
Colombian emerald features <span aria-hidden="true">→</span>
</a>
</li>
</ul>
Expand Down
3 changes: 2 additions & 1 deletion src/components/gallery/FamilyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from '../ui/Button';
import { useFamilyExpressions } from '../../hooks/useFamilies';
import type { MineralFamily, MineralExpression } from '../../lib/db';
import { sanitizeSvg } from '../../lib/sanitize-svg';
import { formatLabel } from '../../lib/format-label';
import { clsx } from 'clsx';

interface FamilyModalProps {
Expand Down Expand Up @@ -297,7 +298,7 @@ export function FamilyModal({ family, onClose }: FamilyModalProps) {
{family.growth_method && (
<div className="flex justify-between items-baseline">
<span className="text-sm text-slate-600">Growth Method</span>
<span className="text-sm font-medium text-slate-900 capitalize">{family.growth_method.replace(/_/g, ' ')}</span>
<span className="text-sm font-medium text-slate-900">{formatLabel(family.growth_method)}</span>
</div>
)}
{family.manufacturer && (
Expand Down
5 changes: 5 additions & 0 deletions src/components/gallery/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ export function FilterBar({
<div className={clsx('space-y-4', className)}>
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<label htmlFor="gallery-search" className="sr-only">
Search minerals
</label>
<SearchInput
id="gallery-search"
name="gallery-search"
placeholder="Search minerals by name, chemistry, or system..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/identification/HardnessReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export function HardnessReference() {
href="/learn/fundamentals/physical-properties"
className="text-amber-600 hover:text-amber-700 underline"
>
Learn more about hardness, toughness, and gem durability →
Learn more about hardness, toughness, and gem durability <span aria-hidden="true">→</span>
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/lab/ChelseaFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export function ChelseaFilter() {
href="/learn/equipment/other-tools"
className="text-crystal-700 hover:text-crystal-700 underline"
>
Learn more about the Chelsea filter and its limitations →
Learn more about the Chelsea filter and its limitations <span aria-hidden="true">→</span>
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/lab/HeavyLiquidReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function HeavyLiquidReference() {
href="/learn/equipment/other-tools"
className="text-crystal-700 hover:text-crystal-700 underline"
>
Learn more about heavy liquids and SG measurement →
Learn more about heavy liquids and SG measurement <span aria-hidden="true">→</span>
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/lab/SpectroscopeCalculator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function SpectroscopeCalculator() {
href="/learn/equipment/spectroscope"
className="text-crystal-700 hover:text-crystal-700 underline"
>
Learn spectroscope technique and absorption spectrum interpretation →
Learn spectroscope technique and absorption spectrum interpretation <span aria-hidden="true">→</span>
</a>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const showPackages = path.startsWith('/docs');
&copy; {currentYear} gemmology.dev. Code under <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer" class="hover:text-slate-700">MIT</a>, content under <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer" class="hover:text-slate-700">CC BY-SA 4.0</a>.
</p>
<p class="text-sm text-slate-600">
Built for the <a href="https://gem-a.com" target="_blank" rel="noopener noreferrer" class="text-crystal-700 hover:text-crystal-700">FGA</a> community
Built for the <a href="https://gem-a.com" target="_blank" rel="noopener noreferrer" class="text-crystal-700 underline underline-offset-2 hover:text-crystal-800">FGA</a> community
</p>
</div>
</div>
Expand Down
11 changes: 8 additions & 3 deletions src/components/minerals/CounterpartsSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import type { Mineral } from '../../lib/db-server';
import { cleanCrystalSvg, isValidSvg } from '../../lib/svg-utils';
import { formatLabel } from '../../lib/format-label';

interface Props {
/** The current mineral being viewed */
Expand All @@ -21,6 +22,10 @@ const isNatural = !mineral.origin || mineral.origin === 'natural';
const hasCounterparts = counterparts.length > 0;
const hasNaturalCounterpart = !!naturalCounterpart;

// Strip the parenthetical expression label (e.g. "Diamond (Octahedron)" → "Diamond")
// — synthetics list is family-scoped, so the expression name is misleading.
const familyName = mineral.name.replace(/\s*\([^)]*\)\s*$/, '').trim();

// Don't render if nothing to show
if (!hasCounterparts && !hasNaturalCounterpart) return;

Expand All @@ -39,7 +44,7 @@ const originStyles: Record<string, string> = {
</h2>
<p class="text-sm text-slate-600 mt-1">
{isNatural
? `Known synthetic and simulant versions of ${mineral.name}`
? `Known synthetic and simulant versions of ${familyName}`
: `The natural gem this ${mineral.origin} imitates`
}
</p>
Expand Down Expand Up @@ -79,7 +84,7 @@ const originStyles: Record<string, string> = {
</span>
</div>
{cp.growth_method && (
<div class="text-sm text-slate-600 capitalize">{cp.growth_method.replace(/_/g, ' ')}</div>
<div class="text-sm text-slate-600">{formatLabel(cp.growth_method)}</div>
)}
</div>
<svg class="w-5 h-5 text-slate-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
Expand Down Expand Up @@ -140,7 +145,7 @@ const originStyles: Record<string, string> = {
href="/gallery?origin=synthetic"
class="text-sm text-sky-600 hover:text-sky-700 font-medium"
>
View all synthetic gems &rarr;
View all synthetic gems <span aria-hidden="true">&rarr;</span>
</a>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/minerals/RelatedMinerals.astro
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const { minerals, currentSystem } = Astro.props;
href={`/gallery?system=${currentSystem.toLowerCase()}`}
class="text-sm text-sky-600 hover:text-sky-700 font-medium"
>
View all {currentSystem} minerals &rarr;
View all {currentSystem} minerals <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
2 changes: 1 addition & 1 deletion src/components/optical/DichroscopeResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function DichroscopeResults() {
href="/learn/equipment/dichroscope"
className="text-purple-600 hover:text-purple-700 underline"
>
Learn dichroscope technique and see full pleochroism reference →
Learn dichroscope technique and see full pleochroism reference <span aria-hidden="true">→</span>
</a>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/components/optical/OpticalTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ export function OpticalTools() {
<ul className="text-sm text-purple-800 space-y-1">
<li>
<a href="/learn/equipment/dichroscope" className="underline hover:text-purple-600">
Dichroscope technique and pleochroism reference →
Dichroscope technique and pleochroism reference <span aria-hidden="true">→</span>
</a>
</li>
<li>
<a href="/learn/fundamentals/optical-properties" className="underline hover:text-purple-600">
Optical properties fundamentals →
Optical properties fundamentals <span aria-hidden="true">→</span>
</a>
</li>
<li>
<a href="/learn/equipment/polariscope" className="underline hover:text-purple-600">
Polariscope interpretation guide →
Polariscope interpretation guide <span aria-hidden="true">→</span>
</a>
</li>
</ul>
Expand Down
4 changes: 2 additions & 2 deletions src/components/quiz/QuizPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ export function QuizPage({ learnEntries }: QuizPageProps) {
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white py-12">
<div className="max-w-4xl mx-auto px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">Quiz</h1>
<h1 className="text-3xl font-bold text-slate-900">Practice and Exam Mode</h1>
<p className="text-slate-600 mt-2">
Test your gemmology knowledge with questions from the learn content.
756 questions across all FGA subject areas. Practise at your own pace, or sit a timed mock exam.
</p>
</div>

Expand Down
2 changes: 1 addition & 1 deletion src/components/tools/ToolsHub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function ToolsHub() {
<div className="max-w-3xl pb-8 border-b border-slate-200">
<h1 className="text-4xl font-bold text-slate-900">Tools</h1>
<p className="mt-4 text-lg text-slate-600">
Professional gemmological tools for calculations, conversions, and reference.
Calculators for the bench: SG by hydrostatic weighing, RI lookup, birefringence, critical angle, carat estimation, and unit conversion &mdash; alongside dichroscope, polariscope, and Chelsea-filter references.
</p>
</div>

Expand Down
18 changes: 18 additions & 0 deletions src/lib/format-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const ACRONYMS = new Set([
'CVD', 'HPHT', 'FTIR', 'HPLC', 'GIA', 'FGA', 'AGTA',
'EDXRF', 'XRF', 'EDS', 'SEM', 'UV', 'IR', 'NIR',
'YAG', 'GGG', 'CZ', 'IGI', 'AGS', 'SSEF', 'GRS',
]);

export function formatLabel(value: string | null | undefined): string {
if (!value) return '';
return value
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const upper = word.toUpperCase();
if (ACRONYMS.has(upper)) return upper;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
}
6 changes: 3 additions & 3 deletions src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const { familyCount } = await getSiteStats();
const popularPages = [
{ title: 'Crystal Gallery', href: '/gallery', description: `Explore ${familyCount} mineral crystal structures` },
{ title: 'Learn Gemmology', href: '/learn', description: 'FGA curriculum and educational content' },
{ title: 'CDL Playground', href: '/playground', description: 'Interactive crystal visualization' },
{ title: 'Quiz', href: '/quiz', description: 'Practice and exam-mode questions across the FGA syllabus' },
{ title: 'Documentation', href: '/docs', description: 'API reference and guides' },
];
---
Expand All @@ -32,11 +32,11 @@ const popularPages = [
</div>
<h1 class="text-3xl sm:text-4xl font-semibold text-slate-900 mb-3">Lost in the lattice</h1>
<p class="text-slate-600 mb-8 max-w-md">
The page you're looking for doesn't exist or has been moved. Try one of the routes below or head back to the gallery.
This page doesn&apos;t crystallise. The URL you followed doesn&apos;t correspond to a known structure &mdash; it may have been moved or never existed.
</p>

<div class="flex flex-wrap justify-center gap-4 mb-12">
<Button href="/" variant="primary">Go Home</Button>
<Button href="/" variant="primary">Return to homepage</Button>
<Button href="/gallery" variant="secondary">Browse Gallery</Button>
<Button href="/learn" variant="ghost">Open Learn</Button>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/gallery/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const initialOrigin = url.searchParams.get('origin') || '';
// Always point to base gallery URL as canonical to prevent duplicate content from filters
const canonicalURL = 'https://gemmology.dev/gallery';

const { familyCount } = await getSiteStats();
const { familyCount, expressionCount } = await getSiteStats();

// SSG-fetch families + filter dropdowns at build time so the gallery first
// paint does not block on sql.js + the 8 MB minerals.db. The wasm DB only
Expand All @@ -33,14 +33,14 @@ const [initialFamilies, initialSystems, initialOrigins, initialGroups] = await P

<BaseLayout
title="Crystal Gallery"
description={`Explore ${familyCount} mineral families with FGA-accurate crystallographic data and interactive visualizations.`}
description={`Explore ${familyCount} mineral families and ${expressionCount} crystal expressions with FGA-accurate crystallographic data.`}
canonical={canonicalURL}
>
<div class="bg-gradient-to-b from-crystal-50 to-white min-h-[80vh]">
<Container size="xl" padding="md">
<SectionHeader
title="Crystal Gallery"
description={`Explore ${familyCount} mineral families with FGA-accurate crystallographic data. Click any crystal to view detailed properties and open in the playground.`}
description={`Explore ${familyCount} mineral families and ${expressionCount} crystal expressions, with FGA-accurate crystallographic data. Click any crystal to view detailed properties and open in the playground.`}
/>
<Gallery
client:load
Expand Down
14 changes: 6 additions & 8 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,12 @@ const iconPaths: Record<string, string> = {
<div class="grid lg:grid-cols-2 gap-12 items-center">
<div>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-slate-900 tracking-tight">
Identify gems with
<span class="gradient-text block">confidence</span>
Crystal morphology,
<span class="gradient-text block">rendered precisely.</span>
</h1>
<p class="mt-6 text-lg text-slate-600 max-w-xl">
Study, practise, and reason about coloured gemstones with an open
FGA-aligned toolkit: a {familyCount}-mineral reference gallery,
interactive crystal morphology, gemmological calculators, and a
timed exam mode.
{familyCount} minerals with FGA-accurate crystallographic data, rendered live from open-source code.
Study, practise, and reason about coloured gemstones &mdash; gallery, calculators, quiz, and timed exam included.
</p>
<div class="mt-8 flex flex-wrap gap-4">
<Button variant="primary" size="lg">
Expand Down Expand Up @@ -154,7 +152,7 @@ const iconPaths: Record<string, string> = {
<section class="py-24 bg-white">
<Container size="xl" padding="none" class="px-4 sm:px-6 lg:px-8">
<SectionHeader
title="Everything you need for crystal visualization"
title="Built for the full FGA curriculum &mdash; and the developers who work with it."
description="A comprehensive toolkit for FGA students, gemmologists, and crystal enthusiasts."
client:load
/>
Expand Down Expand Up @@ -398,7 +396,7 @@ const iconPaths: Record<string, string> = {
<!-- CTA -->
<div class="mt-12 text-center">
<Link href="/quiz" variant="primary" withArrow className="text-base">
Start practicing now
Start practising now
</Link>
</div>
</Container>
Expand Down
21 changes: 4 additions & 17 deletions src/pages/minerals/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import LearnMoreSection from '../../components/minerals/LearnMoreSection.astro';
import TagList from '../../components/minerals/TagList.astro';
import MineralSchema from '../../components/seo/MineralSchema.astro';
import { getAllMinerals, getAllFamilies, getPrimaryExpressionId, getMineralWithModels, getMineralsBySystemWithSvg, getCounterpartMinerals, getNaturalCounterpart, type Mineral } from '../../lib/db-server';
import { formatLabel } from '../../lib/format-label';

export async function getStaticPaths() {
const minerals = await getAllMinerals();
Expand Down Expand Up @@ -78,20 +79,6 @@ const title = `${mineral.name} Crystal Structure`;
const description = `Explore the ${mineral.name} crystal structure, properties, and CDL notation. ${mineral.system} system${mineral.chemistry ? `, ${mineral.chemistry}` : ''}.`;
const canonicalUrl = `https://gemmology.dev/minerals/${mineral.id}`;

// Acronym-aware formatter for free-text identification fields (CVD, HPHT, FTIR, …) — P2-7
const ACRONYMS = new Set(['CVD', 'HPHT', 'FTIR', 'HPLC', 'GIA', 'FGA', 'AGTA', 'EDXRF', 'XRF', 'EDS', 'SEM', 'UV', 'IR', 'NIR', 'YAG', 'GGG', 'CZ']);
function formatLabel(value: string | null | undefined): string {
if (!value) return '';
return value
.replace(/_/g, ' ')
.split(' ')
.map(word => {
const upper = word.toUpperCase();
if (ACRONYMS.has(upper)) return upper;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
}
---

<BaseLayout title={title} description={description}>
Expand All @@ -106,6 +93,9 @@ function formatLabel(value: string | null | undefined): string {
<div class="grid lg:grid-cols-3 gap-8">
<!-- Left Column: Crystal Structure & Properties -->
<div class="lg:col-span-2 space-y-8">
<!-- Crystal Structure (rendered SVG above the fold on every viewport) -->
<CrystalStructureCard mineral={mineral} />

<!-- Quick Facts -->
<QuickFacts mineral={mineral} />

Expand All @@ -114,9 +104,6 @@ function formatLabel(value: string | null | undefined): string {
<PlaygroundLink mineral={mineral} />
</div>

<!-- Crystal Structure -->
<CrystalStructureCard mineral={mineral} />

<!-- Physical Properties -->
<PropertyTable
title="Physical Properties"
Expand Down
Loading