update vite config and added about modal
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ erl_crash.dump
|
|||||||
|
|
||||||
# Ignore assets that are produced by build tools.
|
# Ignore assets that are produced by build tools.
|
||||||
/priv/static/assets/
|
/priv/static/assets/
|
||||||
|
/priv/static/chunks/
|
||||||
/priv/static/*.js
|
/priv/static/*.js
|
||||||
/priv/static/*.html
|
/priv/static/*.html
|
||||||
/priv/static/*.css
|
/priv/static/*.css
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ defmodule MoundHuntersWeb.Router do
|
|||||||
if File.exists?(index_path) do
|
if File.exists?(index_path) do
|
||||||
conn
|
conn
|
||||||
|> put_resp_header("content-type", "text/html; charset=utf-8")
|
|> put_resp_header("content-type", "text/html; charset=utf-8")
|
||||||
|> put_resp_header("cache-control", "no-cache")
|
|> put_resp_header("cache-control", "no-cache, must-revalidate")
|
||||||
|> send_file(200, index_path)
|
|> send_file(200, index_path)
|
||||||
else
|
else
|
||||||
send_resp(conn, 404, "Not found")
|
send_resp(conn, 404, "Not found")
|
||||||
|
|||||||
358
ui/src/components/AboutModal.vue
Normal file
358
ui/src/components/AboutModal.vue
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal">
|
||||||
|
<div v-if="isOpen" class="modal-backdrop" @click="closeModal">
|
||||||
|
<div class="modal-content" @click.stop>
|
||||||
|
<button class="close-button" @click="closeModal" aria-label="Close">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2>About This Project</h2>
|
||||||
|
|
||||||
|
<section class="intro">
|
||||||
|
<p>
|
||||||
|
Around 2,000 years ago, the Hopewell culture built a 60-mile ceremonial road
|
||||||
|
connecting Newark to Chillicothe, Ohio. Most of it has been destroyed by farming,
|
||||||
|
but tiny remnants might still be hiding in the landscape—parallel earthen walls
|
||||||
|
just 50cm tall, visible only as subtle shadows in the right light.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
There's way too much terrain for archaeologists to analyze alone. That's where
|
||||||
|
you come in! This tool lets you explore high-resolution lidar data and help
|
||||||
|
search for lost sections of the Great Hopewell Road.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="video-section">
|
||||||
|
<h3>Learn More About the Road</h3>
|
||||||
|
<div class="video-container">
|
||||||
|
<iframe
|
||||||
|
src="https://www.youtube.com/embed/Ltu2hJwqId8"
|
||||||
|
title="The Great Hopewell Road"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="instructions">
|
||||||
|
<h3>How to Use This Tool</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Start exploring:</strong> Check out the known archaeological sites on the map.
|
||||||
|
Click the pin icon next to each site name to fly there and see the crisp lidar imagery.
|
||||||
|
Click on any of the pins on the map to learn more about the cite, and follow up on
|
||||||
|
the cited references to dive further down the rabbit hole.
|
||||||
|
Use the sidebar controls to customize what you see.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Hunt for the road:</strong> Right-click anywhere on the map (within Ohio) to
|
||||||
|
request a tile. If someone's already requested it, you can load the interactive data
|
||||||
|
instantly. Otherwise, you'll wait a bit for the processing pipeline.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Adjust the lighting:</strong> Once a tile loads, the shading sandbox opens.
|
||||||
|
Change the lighting direction and exaggerate the terrain to increase contrast—this
|
||||||
|
makes subtle features pop out. Click "Render Tile" to add it to the map.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Mark your finds:</strong> Found something interesting? Drop a pin using the
|
||||||
|
geometry tools in the top right. Then share it on
|
||||||
|
<a href="http://discord.gg/miniminuteclan" target="_blank" rel="noopener noreferrer">
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
with coordinates and a screenshot!
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="about-author">
|
||||||
|
<h3>About me</h3>
|
||||||
|
<p>
|
||||||
|
I'm Mark Kalsbeek, a web developer and researcher from the Netherlands.
|
||||||
|
I was inspired to build this after watching Milo's video above. If you want to
|
||||||
|
reach me, @ me on
|
||||||
|
<a href="http://discord.gg/miniminuteclan" target="_blank" rel="noopener noreferrer">
|
||||||
|
Milo's Discord
|
||||||
|
</a>:
|
||||||
|
<span v-if="!usernameRevealed" class="username-reveal" @click="revealUsername">
|
||||||
|
reveal
|
||||||
|
</span>
|
||||||
|
<span v-else class="username">{{ username }}</span>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="thanks">
|
||||||
|
<h3>Thanks</h3>
|
||||||
|
<p>
|
||||||
|
Special thanks to <a href="https://gis1.oit.ohio.gov/geodatadownload/" target="_blank" rel="noopener noreferrer">Ohio OGRIP</a>
|
||||||
|
for making high-quality lidar data freely available with an easy-to-reverse API.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Map tiles from <a href="https://www.openstreetmap.org/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors
|
||||||
|
and satellite imagery from <a href="https://www.esri.com/" target="_blank" rel="noopener noreferrer">Esri</a>.
|
||||||
|
</p>
|
||||||
|
<p class="tech-stack">
|
||||||
|
Built with: Elixir (plug_cowboy, jason, geo, logger_file_backend, httpoison, mime),
|
||||||
|
Python (laspy, scipy, numpy, pyproj),
|
||||||
|
Deno (Vue, Vite, Three.js, MapLibre GL, Pinia),
|
||||||
|
and of course Claude for infinite amounts of grunt work.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const MODAL_VERSION = '1.0';
|
||||||
|
const STORAGE_KEY = 'aboutModalVersion';
|
||||||
|
const OBFUSCATED_USERNAME = '404d61726b6b313136';
|
||||||
|
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const usernameRevealed = ref(false);
|
||||||
|
const username = ref('');
|
||||||
|
const seenRecentVersion = ref(true);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const revealUsername = () => {
|
||||||
|
username.value = OBFUSCATED_USERNAME.match(/.{2}/g)
|
||||||
|
.map(hex => String.fromCharCode(parseInt(hex, 16)))
|
||||||
|
.join('');
|
||||||
|
usernameRevealed.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
isOpen.value = newVal;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(isOpen, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal);
|
||||||
|
if (!newVal) {
|
||||||
|
// Mark as seen when closed
|
||||||
|
localStorage.setItem(STORAGE_KEY, MODAL_VERSION);
|
||||||
|
seenRecentVersion.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
isOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if should show on mount
|
||||||
|
onMounted(() => {
|
||||||
|
const seenVersion = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (seenVersion !== MODAL_VERSION) {
|
||||||
|
seenRecentVersion.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose methods for parent component
|
||||||
|
defineExpose({
|
||||||
|
open: () => { isOpen.value = true; },
|
||||||
|
seenRecentVersion,
|
||||||
|
close: closeModal
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 60vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-content {
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
padding: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.2s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 2rem;
|
||||||
|
padding-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro p:first-child {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||||
|
margin: 1rem 0;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions p {
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions strong {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-reveal {
|
||||||
|
display: inline-block;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-reveal:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-family: monospace;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-stack {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal transitions */
|
||||||
|
.modal-enter-active,
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from,
|
||||||
|
.modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active .modal-content,
|
||||||
|
.modal-leave-active .modal-content {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from .modal-content,
|
||||||
|
.modal-leave-to .modal-content {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layer-controls">
|
<div class="layer-controls">
|
||||||
|
<button
|
||||||
|
class="about-button"
|
||||||
|
:class="{ 'pulse-highlight': !aboutRef?.seenRecentVersion }"
|
||||||
|
@click="aboutRef.open"
|
||||||
|
>
|
||||||
|
About this App
|
||||||
|
</button>
|
||||||
|
<AboutModal ref="aboutRef"/>
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
<!-- BASE MAP SELECTION -->
|
<!-- BASE MAP SELECTION -->
|
||||||
<!-- ============================================= -->
|
<!-- ============================================= -->
|
||||||
@@ -110,7 +118,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
import { KNOWN_SITES } from '../data/historicSites.js';
|
import { KNOWN_SITES } from '../data/historicSites.js';
|
||||||
|
import AboutModal from './AboutModal.vue';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACE
|
// INTERFACE
|
||||||
@@ -155,6 +165,8 @@ defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const aboutRef = ref(null);
|
||||||
|
|
||||||
defineEmits([
|
defineEmits([
|
||||||
'update:baseLayer',
|
'update:baseLayer',
|
||||||
'update:historicMarkersExpanded',
|
'update:historicMarkersExpanded',
|
||||||
@@ -194,6 +206,42 @@ const sites = KNOWN_SITES;
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================= */
|
||||||
|
/* About button */
|
||||||
|
/* ============================================= */
|
||||||
|
|
||||||
|
.about-button {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(180deg, #4B91F7 0%, #367AF6 100%);
|
||||||
|
background-origin: border-box;
|
||||||
|
box-shadow: 0px 0.5px 1.5px rgba(54, 122, 246, 0.25), inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-button:focus {
|
||||||
|
box-shadow: inset 0px 0.8px 0px -0.25px rgba(255, 255, 255, 0.2), 0px 0.5px 1.5px rgba(54, 122, 246, 0.25), 0px 0px 0px 3.5px rgba(58, 108, 217, 0.5);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 20px rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-highlight {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================= */
|
/* ============================================= */
|
||||||
/* CONTROL SECTIONS */
|
/* CONTROL SECTIONS */
|
||||||
/* ============================================= */
|
/* ============================================= */
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ export default defineConfig({
|
|||||||
main: resolve(__dirname, 'index.html'),
|
main: resolve(__dirname, 'index.html'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'app.js',
|
entryFileNames: 'app-[hash].js',
|
||||||
chunkFileNames: 'chunks/[name].js',
|
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||||
assetFileNames: 'assets/[name].[ext]'
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||||
|
manualChunks: {
|
||||||
|
'vendor': ['vue', 'pinia'],
|
||||||
|
'three': ['three'],
|
||||||
|
'maplibre': ['maplibre-gl']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user