non-alprs

This commit is contained in:
Will Freeman
2025-10-05 12:53:09 -06:00
parent 97a3bf4703
commit 5d1ea9653b
15 changed files with 528 additions and 78 deletions

View File

@@ -0,0 +1,126 @@
<template>
<v-dialog
v-model="show"
:max-width="$vuetify.display.mobile ? undefined : '600'"
:fullscreen="$vuetify.display.mobile"
:transition="$vuetify.display.mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
@click:outside="dismiss"
>
<v-card class="h-100 d-flex flex-column">
<v-card-title class="text-center py-4 font-weight-bold bg-warning d-flex align-center justify-center">
<v-icon icon="mdi-alert-circle" size="large" class="mr-2"></v-icon>
<h3 class="headline">Are you sure it's an ALPR?</h3>
<v-spacer v-if="$vuetify.display.mobile"></v-spacer>
<v-btn
v-if="$vuetify.display.mobile"
icon="mdi-close"
variant="text"
color="on-warning"
@click="dismiss"
></v-btn>
</v-card-title>
<v-card-text class="pa-6 flex-grow-1 d-flex flex-column justify-center">
<div class="content-wrapper">
<div class="text-center mb-6">
<img
src="/alprs/flock-1.jpg"
alt="Example ALPR Camera"
style="max-width: 220px; width: 100%; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);"
/>
<div class="mt-2 text-caption text-grey-darken-1">
Example ALPR camera (Flock Safety)
</div>
</div>
<p class="text-center text-body-1 mb-12">
Not every camera on a pole is an ALPR. Many are just traffic monitoring cameras or other equipment.
</p>
<div class="text-center">
<v-btn
color="primary"
variant="elevated"
size="large"
to="/identify"
prepend-icon="mdi-image-search"
class="mb-3"
@click="dismiss"
>
View ALPR Gallery
</v-btn>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
width="100%"
color="success"
variant="text"
size="large"
@click="dismiss"
>
Continue
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const show = ref(false);
onMounted(() => {
if (!localStorage.getItem('alpr-verification-acknowledged')) {
show.value = true;
}
});
function dismiss() {
show.value = false;
localStorage.setItem('alpr-verification-acknowledged', 'true');
}
</script>
<style scoped>
.headline {
color: rgb(var(--v-theme-on-warning));
}
ul {
padding-left: 1.2rem;
}
li {
margin-bottom: 0.25rem;
}
/* Mobile fullscreen styling */
@media (max-width: 960px) {
.content-wrapper {
max-width: 100%;
}
.v-card-title h3 {
font-size: 1.25rem !important;
}
.v-card-text {
padding: 1.5rem !important;
}
.v-card-actions {
padding: 1.5rem !important;
flex-direction: column;
gap: 0.75rem;
}
.v-card-actions .v-btn {
width: 100%;
margin: 0 !important;
}
}
</style>

View File

@@ -1,9 +1,10 @@
<template>
<v-container fluid>
<v-container fluid style="padding: 0;">
<v-row
justify="center"
class="hero text-center mb-4"
:style="`background: url('${imageUrl}') no-repeat center center / cover; --hero-opacity: ${opacity};`"
:class="{ 'hero-image': imageUrl, 'hero-gradient': gradient }"
:style="heroStyle"
>
<v-col cols="12" md="8">
<h1 class="mb-4">{{ title }}</h1>
@@ -32,6 +33,7 @@ const props = defineProps({
title: String,
description: String,
imageUrl: String,
gradient: String,
buttonText: String,
buttonTo: String,
buttonHref: String,
@@ -44,6 +46,15 @@ const props = defineProps({
const target = computed(() =>
props.buttonHref && !props.buttonHref.startsWith('#') ? '_blank' : '_self'
);
const heroStyle = computed(() => {
if (props.gradient) {
return `background: ${props.gradient};`;
} else if (props.imageUrl) {
return `background: url('${props.imageUrl}') no-repeat center center / cover; --hero-opacity: ${props.opacity};`;
}
return '';
});
</script>
<style scoped>
@@ -51,9 +62,11 @@ const target = computed(() =>
color: white;
padding: 100px 0 !important;
position: relative;
min-height: 350px;
}
.hero::before {
/* Overlay for image backgrounds only */
.hero-image::before {
content: '';
position: absolute;
top: 0;

View File

@@ -131,6 +131,14 @@ const router = createRouter({
name: 'foia',
component: () => import('../views/FOIA.vue'),
},
{
path: '/identify',
name: 'identify',
component: () => import('../views/Identification.vue'),
meta: {
title: 'Identify ALPRs | DeFlock'
}
},
{
path: '/press',
name: 'press',

View File

@@ -0,0 +1,250 @@
<template>
<!-- Hero Section -->
<Hero
title="Spot an ALPR"
description="Visual guide to identifying license plate readers"
gradient="linear-gradient(135deg, rgb(var(--v-theme-primary)) 0%, rgb(var(--v-theme-secondary)) 100%)"
/>
<v-container fluid>
<!-- Flock Safety - Featured Section -->
<v-container>
<v-card class="featured-card" elevation="4">
<v-card-title class="text-center bg-green text-h4 pt-6 px-6">
<v-img
contain
src="/vendor-logos/Flock_Safety_Logo.svg"
:alt="'Flock Logo'"
style="height: 48px; filter: invert(1);"
/>
</v-card-title>
<v-card-subtitle class="text-center pa-4 text-h6" style="white-space: normal; word-break: break-word;">
black housing teardrop shape usually with solar panels
</v-card-subtitle>
<v-card-text class="pa-6">
<v-row>
<v-col v-for="(image, index) in flockImages" :key="index" cols="12" sm="6" md="3">
<v-card class="image-card" elevation="2" @click="openImageInNewTab(image)">
<v-img
:src="image"
:aspect-ratio="4/3"
cover
class="cursor-pointer"
>
<template v-slot:placeholder>
<v-row class="fill-height ma-0" align="center" justify="center">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
</v-row>
</template>
</v-img>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
<!-- Other ALPR Types -->
<v-container class="mb-12">
<h2 class="text-center mb-8">Other ALPR Types</h2>
<v-row>
<v-col cols="12" md="6" v-for="vendor in otherVendors" :key="vendor.vendor" class="mb-4">
<v-card class="vendor-card h-100" elevation="2">
<v-card-title class="text-center" style="background-color: #f5f5f5;">
<v-img v-if="vendor.logoUrl" contain :src="vendor.logoUrl" :alt="`${vendor.vendor} Logo`" style="height: 48px;" />
<div
style="height: 48px; display: flex; align-items: center; justify-content: center;"
class="font-weight-bold"
v-else
>
{{ vendor.vendor }}
</div>
</v-card-title>
<v-card-text class="pa-4">
<v-row>
<v-col v-for="(image, index) in vendor.imageUrls" :key="index" cols="6">
<v-card class="image-card" elevation="1" @click="openImageInNewTab(image)">
<v-img
:src="image"
:aspect-ratio="4/3"
cover
class="cursor-pointer"
>
</v-img>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<!-- What ALPRs are NOT -->
<v-container class="mb-12">
<v-card class="not-alpr-card" elevation="3">
<v-card-title
class="text-center px-6 bg-warning font-weight-bold"
:class="[$vuetify.display.smAndDown ? 'text-h6' : 'text-h4']"
style="white-space: normal; word-break: break-word;"
>
NOT ALPRs
</v-card-title>
<v-card-text class="px-6 pt-6">
<v-row>
<!-- Traffic Detection Cameras -->
<v-col cols="12" md="9" class="mb-6">
<h3 class="text-center mb-4">Traffic Detection Cameras</h3>
<v-row>
<v-col v-for="(image, index) in trafficCameraImages" :key="index" cols="12" md="4">
<v-card class="image-card" elevation="1" @click="openImageInNewTab(image)">
<v-img
:src="image"
:aspect-ratio="4/3"
cover
class="cursor-pointer"
/>
</v-card>
</v-col>
</v-row>
</v-col>
<!-- Snow Detection Cameras -->
<v-col cols="12" md="3" class="mb-6">
<h3 class="text-center mb-4">Snow/Ice Cameras</h3>
<v-row>
<v-col v-for="(image, index) in snowDetectionImages" :key="index" cols="12">
<v-card class="image-card" elevation="1" @click="openImageInNewTab(image)">
<v-img
:src="image"
:aspect-ratio="4/3"
cover
class="cursor-pointer"
/>
</v-card>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
<!-- Action Section -->
<v-container class="text-center mb-12">
<v-card class="action-card pa-8" elevation="0" color="transparent">
<v-card-title class="text-h4 mb-4">Found one?</v-card-title>
<v-card-text>
<v-btn
size="x-large"
color="primary"
to="/report"
prepend-icon="mdi-map-marker-plus"
variant="elevated"
class="mr-4"
>
Add to Map
</v-btn>
</v-card-text>
</v-card>
</v-container>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import Hero from '@/components/layout/Hero.vue';
import Footer from '@/components/layout/Footer.vue';
function openImageInNewTab(url: string) {
window.open(url, '_blank');
}
// Flock Safety - Featured prominently
const flockImages = [
'/alprs/flock-5.webp',
'/alprs/flock-1.jpg',
'/alprs/flock-2.jpg',
'/alprs/flock-3.jpg',
'/alprs/flock-4.jpg'
];
// Other ALPR vendors
const otherVendors = [
{
vendor: 'Motorola/Vigilant',
logoUrl: '/vendor-logos/Motorola_Solutions_Logo.svg',
description: 'Usually dark and rectangular, with visible IR illuminators. Often next to an ugly white box.',
imageUrls: ['/alprs/motorola-1.jpg', '/alprs/motorola-2.jpg', '/alprs/motorola-3.jpg', '/alprs/motorola-4.jpg']
},
{
vendor: 'Genetec',
logoUrl: '/vendor-logos/logo_genetec_rgb_color_tm.svg',
description: 'Usually white and rectangular, with visible IR illuminators',
imageUrls: ['/alprs/genetec-1.webp', '/alprs/genetec-2.webp', '/alprs/genetec-3.webp']
},
{
vendor: 'Leonardo/ELSAG',
logoUrl: '/vendor-logos/Logo_Leonardo.svg',
description: 'Large, highly visible array of IR lights',
imageUrls: ['/alprs/elsag-3.jpg', '/alprs/elsag-4.jpg', '/alprs/elsag-1.jpg', '/alprs/elsag-2.jpg']
},
{
vendor: 'Neology',
description: 'Ugly with a white rounded rectangular shape and long hood',
imageUrls: ['/alprs/neology-1.jpg', '/alprs/neology-2.jpg']
}
];
const trafficCameraImages = [
'/non-alprs/iteris.webp',
'/non-alprs/traffic-cam.webp',
'/non-alprs/flir.webp',
];
const snowDetectionImages = [
'/non-alprs/frost-cam.jpeg'
];
</script>
<style scoped>
.featured-card {
/* border: 3px solid rgb(var(--v-theme-secondary)); */
margin-bottom: 2rem;
}
.vendor-card {
transition: transform 0.2s ease-in-out;
}
.vendor-card:hover {
transform: translateY(-2px);
}
.image-card {
transition: transform 0.2s ease-in-out;
cursor: pointer;
}
.image-card:hover {
transform: scale(1.05);
}
.cursor-pointer {
cursor: pointer;
}
.overlay-badge {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
}
h1, h2, h3 {
color: rgb(var(--v-theme-on-surface));
}
</style>

View File

@@ -1,17 +1,32 @@
<template>
<v-container class="my-16">
<v-row justify="center" class="mb-8">
<!-- ALPR Verification Dialog -->
<ALPRVerificationDialog />
<v-row justify="center" class="mb-4">
<v-col cols="12" md="8">
<h2 class="text-center text-h4 font-weight-bold">Choose Your Reporting Method</h2>
<p class="text-center text-body-1 mt-4">
We use OpenStreetMap to source our data. Choose a method below to add to our map.
</p>
<p class="sans-serif text-center mt-2">
<a style="font-size: 0.85em" target="_blank" href="https://deflock.hashnode.dev/why-we-use-openstreetmap">Why do we use OSM?</a>
Choose a method below to add to our map.
</p>
</v-col>
</v-row>
<!-- ALPR Identification Warning -->
<v-row justify="center" class="mb-8">
<v-col cols="12" class="text-center">
<v-btn
color="blue"
variant="tonal"
to="/identify"
prepend-icon="mdi-image-search"
size="large"
>
View ALPR Gallery
</v-btn>
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12" md="5" class="pa-4">
<v-card
@@ -88,3 +103,18 @@
</v-row>
</v-container>
</template>
<script setup lang="ts">
import ALPRVerificationDialog from '@/components/ALPRVerificationDialog.vue';
</script>
<style scoped>
.verification-alert {
border-left-width: 6px !important;
}
.verification-alert .v-alert-title {
font-weight: 600;
font-size: 1.1rem;
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<v-container fluid>
<v-row justify="center" class="hero-section-whatis text-center mb-4">
</v-row>
</v-container>
<Hero
image-url="/flock-camera.jpeg"
:opacity="0"
/>
<v-container class="info-section">
<v-container class="info-section mb-16">
<h1 class="mt-0">What are ALPRs?</h1>
<p>
Automated License Plate Readers (ALPRs) are AI-powered cameras that capture and analyze images of all passing vehicles, storing details like your car's location, date, and time. They also capture your car's make, model, color, and identifying features such as dents, roof racks, and bumper stickers, <a href="https://www.flocksafety.com/products/license-plate-readers#:~:text=No%20Plate%3F%20No%20Problem" target="_blank">often turning these into searchable data points</a>. These cameras collect data on millions of vehiclesregardless of whether the driver is suspected of a crime. While these systems can be useful for tracking stolen cars or wanted individuals, they are mostly used to track the movements of innocent people.
@@ -12,6 +12,21 @@
<p>For a detailed explanation of how ALPRs are a threat to privacy, see this <a href="https://www.aclu.org/issues/privacy-technology/you-are-being-tracked" target="_blank">ACLU article</a> as well as this <a href="https://sls.eff.org/technologies/automated-license-plate-readers-alprs" target="_blank">EFF article</a> on ALPRs.</p>
<v-divider class="my-12" />
<h2 class="mb-8">
<v-icon class="mr-2" color="primary">mdi-camera-outline</v-icon>
What do they look like?
</h2>
<div class="mb-16 text-center">
<v-btn size="large" color="primary" to="/identify">
<v-icon left class="mr-2">mdi-image-search</v-icon>
View ALPR Images
</v-btn>
</div>
<v-divider class="my-12" />
<h2>Why Should You Be Concerned</h2>
<p class="mb-8 text-center">
ALPRs invade your privacy and violate your civil liberties. Here's how:
@@ -19,73 +34,19 @@
<Dangers />
<v-divider class="my-12" />
<h2>Frequently Asked Questions</h2>
<h2 class="mb-8">Frequently Asked Questions</h2>
<FAQ />
<h2 id="photos">What they Look Like</h2>
<v-row v-for="vendor in vendors" :key="vendor.vendor" class="mb-4">
<v-col cols="12">
<h3 class="text-center">{{ vendor.vendor }}</h3>
</v-col>
<v-col v-for="url in vendor.imageUrls" cols="12" sm="6" md="4">
<v-img @click="openImageInNewTab(url)" style="cursor: pointer;" cover :aspect-ratio="3/2" :src="url" />
</v-col>
</v-row>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import { ref, type Ref } from 'vue';
import { useDisplay } from 'vuetify';
import Hero from '@/components/layout/Hero.vue';
import Dangers from '@/components/Dangers.vue';
import FAQ from '@/components/FAQ.vue';
import Footer from '@/components/layout/Footer.vue';
const { xs: isMobile } = useDisplay();
const showFullScreenImage = ref(false);
const fullScreenImage: Ref<any|undefined> = ref(undefined);
function openImageInNewTab(url: string, newTab: boolean = true) {
window.open(url, newTab ? '_blank' : '_self');
}
const vendors = [
{
vendor: 'Flock',
count: 4,
urlScheme: '/alprs/flock-{index}.jpg',
},
{
vendor: 'Motorola/Vigilant',
count: 4,
urlScheme: '/alprs/motorola-{index}.jpg',
},
{
vendor: 'Genetec',
count: 3,
urlScheme: '/alprs/genetec-{index}.webp',
},
{
vendor: 'Leonardo/ELSAG',
count: 4,
urlScheme: '/alprs/elsag-{index}.jpg',
},
{
vendor: 'Neology',
count: 2,
urlScheme: '/alprs/neology-{index}.jpg',
},
].reduce((acc: any, vendor: any) => {
const imageUrls = Array.from({ length: vendor.count }, (_, index) =>
vendor.urlScheme.replace('{index}', String(index + 1)),
);
acc.push({ vendor: vendor.vendor, imageUrls });
return acc;
}, []);
</script>
<style scoped>
@@ -129,12 +90,4 @@ h1, h2 {
.fade-enter, .fade-leave-to {
opacity: 0;
}
.hero-section-whatis {
background: url('/flock-camera.jpeg') no-repeat right center;
background-size: cover;
color: white;
position: relative;
height: 400px;
}
</style>