Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add a readme for your dashboard here. Include content overview, data citations, and any relevant technical details.
This dashboard is used to help people with few wineknowledge to choose wine from france depends on their needs in wine type, foodpairing and wine flavor. The specific recommendation result will end in wine production region, as different wineary would have slight differences in making wine, and there are too many winearies to map.
150 changes: 150 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
font-family: system-ui, -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, oxygen, ubuntu, cantarell, sans-serif;
}

header {
padding: 0.75rem 1.25rem;
background-color: #5e111a;
color: #fff;
}

header h1 {
margin: 0;
font-size: 1.5rem;
}

header .info {
margin: 0.25rem 0 0;
font-size: 0.9rem;
opacity: 0.9;
}

div.selection-panel {
margin: 0.2rem 0 0;
overflow-y: auto;
background-color: #78232d;
font-size: 0.85rem;
color: #fff;
}

main {
flex-grow: 1;
padding: 0;
overflow: hidden;
}

.visualization-container {
height: 100%;
width: 100%;
}

footer {
padding: 0.5rem 1.25rem;
background-color: #f8f9fa;
font-size: 0.85rem;
border-top: 1px solid #dee2e6;
}

footer p {
margin: 0;
}

.wine-popup h3 {
margin: 0 0 0.5rem;
color: #722f37;
font-size: 1.1rem;
}

div.selection-panel h3 {
margin: 0.5rem 0 0.25rem;
font-size: 0.9rem;
}

.wine-popup p {
margin: 0.25rem 0;
font-size: 0.9rem;
line-height: 1.3;
}

@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-5px); }
60% { transform: translateY(-2px); }
}


@media screen and (width < 960px) {
.form-container {
width: 85vw;
margin-left: auto;
margin-right: auto;
}

}

.region-selector {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.4rem 0 0.6rem;
}

.region-selector select {
background-color: rgb(60 10 16 / 60%);
color: #fff;
font-size: 0.8rem;
padding: 0.4rem 2rem 0.4rem 0.75rem;
border: 1px solid rgb(255 255 255 / 25%);
border-radius: 6px;
transition: all 0.2s ease;
}

.hidden {
display: none !important;
}


.popup {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
/* stylelint-disable-next-line color-function-alias-notation */
background-color: rgb(0 0 0 / 50%); /* Dim background */
display: flex;
justify-content: center;
align-items: center;
z-index: 2000; /* Above Leaflet map */
}

.popup-content {
background-color: #fff;
color: #333;
padding: 1.5rem 2rem;
border-radius: 8px;
/* stylelint-disable-next-line color-function-alias-notation */
box-shadow: 0 4px 15px rgb(0 0 0 / 30%);
text-align: center;
}

/* stylelint-disable-next-line selector-id-pattern */
#closeBtn {
margin-top: 1rem;
padding: 0.4rem 1.2rem;
background-color: #78232d;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* stylelint-disable-next-line selector-id-pattern */
#closeBtn:hover {
background-color: #5e111a;
}
143 changes: 143 additions & 0 deletions data/wine.geojson

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>France Wine Map</title>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<link rel="stylesheet" href="css/styles.css">
</head>


<body>
<header>
<h1>France Wine Map</h1>
<p class="info">Explore the regions and appellations of French wine</p>
</header>

<div id="infoPopup" class="popup">
<div class="popup-content">
<p>This dashboard helps you explore potential wine region based on your needs in food pairing and flavor preferences. Feel free to click based on type, pairing and flavors.</p>
<button id="closeBtn">Close</button>
</div>
</div>

<div class="selection-panel" id="filter-panel">
<h3>Filter by Wine Type</h3>
<label><input type="checkbox" name="type" value="Red"> Red</label>
<label><input type="checkbox" name="type" value="White"> White</label>
<label><input type="checkbox" name="type" value="Rosé"> Rosé</label>
<label><input type="checkbox" name="type" value="Sparkling"> Sparkling</label>
<h3>Filter by Food Pairing</h3>
<label><input type="checkbox" name="foodpair" value="cheese"> Cheese</label>
<label><input type="checkbox" name="foodpair" value="seafood"> Seafood</label>
<label><input type="checkbox" name="foodpair" value="white meat"> White Meat</label>
<label><input type="checkbox" name="foodpair" value="red meat"> Red Meat</label>
<label><input type="checkbox" name="foodpair" value="vegetables"> Vegetables</label>
<label><input type="checkbox" name="foodpair" value="dessert"> Dessert</label>
<h3>Select a Flavor</h3>
<div class="region-selector">
<label for="flavor-select">Flavor:</label>
<select id="flavor-select" name="flavor">
<option value="">All Flavors</option>
<option value="blackberry">Blackberry</option>
<option value="cherry">Cherry</option>
<option value="green apple">Green Apple</option>
<option value="plum">Plum</option>
<option value="peach">Peach</option>
<option value="citrus">Citrus</option>
<option value="honey">Honey</option>
<option value="truffle">Truffle</option>
<option value="pepper">Pepper</option>
<option value="leather">Leather</option>
<option value="mineral">Mineral</option>
</select>
</div>
</div>

<main>
<div id="map" class="map visualization-container"></div>
</main>
<script type="module" src="js/app.js"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { initFranceWineMap, loadwinelayer, updateWineLayer } from './wine-map.js';

const map = initFranceWineMap('map');
const popup = document.getElementById('infoPopup');
const closeBtn = document.getElementById('closeBtn');

if (closeBtn) {
closeBtn.addEventListener('click', () => {
popup?.classList.add('hidden');
});
}

loadwinelayer(map).then((layer) => {
window.winelayer = layer;
import('./ui.js').then((uiModule) => {
if (uiModule.initForm) {
uiModule.initForm(
'filter-panel',
window.allWineData?.features || [],
(filtered) => {
updateWineLayer(window.winelayer, filtered);
},
);
}
});
});
65 changes: 65 additions & 0 deletions js/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Helper to normalize strings
*/
const normalizeText = (str) =>
(str || '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.trim();

/**
* Filter wine features
*/
function filtertype(selectedTypes = [], features = []) {
if (selectedTypes.length === 0) return features;

return features.filter((feature) => {
const wineType = feature.properties.type || '';
const featureTypes = wineType.split(',').map((t) => normalizeText(t));

return selectedTypes.some((selected) => {
const normSelected = normalizeText(selected);
return featureTypes.some((ft) => ft.includes(normSelected));
});
});
}

function filterfood(selectedfood = [], features = []) {
if (selectedfood.length === 0) return features;

return features.filter((feature) => {
const food = feature.properties.foodpair || '';
const featureFood = food.split(',').map((f) => normalizeText(f));

return selectedfood.some((selected) => {
const normSelected = normalizeText(selected);
return featureFood.some((ff) => ff.includes(normSelected));
});
});
}

function filterflavor(selectedflavor = '', features = []) {
const normSelected = normalizeText(selectedflavor);
if (selectedflavor === '' || selectedflavor.trim() === '') return features;

return features.filter((feature) => {
const flavor = feature.properties.flavor || '';
const featureFlavors = flavor.split(',').map((f) => normalizeText(f));
return featureFlavors.some((ff) => ff.includes(normSelected));
});
}

function filterWineData(features = [], selectedTypes = [], selectedfood = [], selectedflavor = '') {
let result = filtertype(selectedTypes, features);
result = filterfood(selectedfood, result);
result = filterflavor(selectedflavor, result);
return result;
}

export {
filtertype,
filterfood,
filterflavor,
filterWineData,
};
45 changes: 45 additions & 0 deletions js/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { filterWineData } from './filter.js';

function initForm(el, regions, onFilter = null) {
const containEL = typeof el === 'string' ? document.getElementById(el) : el;
if (!containEL) {
throw new Error('A valid DOM element or selector must be provided for the form.');
}

const allregions = regions || [];

// --- Helper: read all controls and apply filters ---
function applyFilters() {
const selectedTypes = Array.from(
containEL.querySelectorAll('input[name="type"]:checked'),
).map((box) => box.value);

const selectedfood = Array.from(
containEL.querySelectorAll('input[name="foodpair"]:checked'),
).map((box) => box.value);

const flavorseleced = containEL.querySelector('#flavor-select');
const selectedflavor = flavorseleced ? flavorseleced.value : '';

let filtered = filterWineData(allregions, selectedTypes, selectedfood, selectedflavor);

if (onFilter) {
onFilter(filtered);
}
}

containEL.querySelectorAll('#flavor-select').forEach((a) => {
a.addEventListener('change', applyFilters);
});

containEL.querySelectorAll('input[name="type"]').forEach((a) => {
a.addEventListener('change', applyFilters);
});

containEL.querySelectorAll('input[name="foodpair"]').forEach((a) => {
a.addEventListener('change', applyFilters);
});
}

export { initForm,
};
Loading