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.
|
||||
/priv/static/assets/
|
||||
/priv/static/chunks/
|
||||
/priv/static/*.js
|
||||
/priv/static/*.html
|
||||
/priv/static/*.css
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule MoundHuntersWeb.Router do
|
||||
if File.exists?(index_path) do
|
||||
conn
|
||||
|> 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)
|
||||
else
|
||||
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>
|
||||
<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 -->
|
||||
<!-- ============================================= -->
|
||||
@@ -110,7 +118,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { KNOWN_SITES } from '../data/historicSites.js';
|
||||
import AboutModal from './AboutModal.vue';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACE
|
||||
@@ -155,6 +165,8 @@ defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const aboutRef = ref(null);
|
||||
|
||||
defineEmits([
|
||||
'update:baseLayer',
|
||||
'update:historicMarkersExpanded',
|
||||
@@ -194,6 +206,42 @@ const sites = KNOWN_SITES;
|
||||
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 */
|
||||
/* ============================================= */
|
||||
|
||||
@@ -20,9 +20,14 @@ export default defineConfig({
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'app.js',
|
||||
chunkFileNames: 'chunks/[name].js',
|
||||
assetFileNames: 'assets/[name].[ext]'
|
||||
entryFileNames: 'app-[hash].js',
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
manualChunks: {
|
||||
'vendor': ['vue', 'pinia'],
|
||||
'three': ['three'],
|
||||
'maplibre': ['maplibre-gl']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user