use iconify

This commit is contained in:
Will Freeman
2025-12-08 22:34:05 -07:00
parent a5a701255a
commit 859dc5963d
26 changed files with 583 additions and 352 deletions

View File

@@ -8,6 +8,8 @@
"name": "deflock",
"version": "0.0.0",
"dependencies": {
"@iconify-vue/ic": "^1.0.1",
"@iconify-vue/mdi": "^1.0.1",
"@types/leaflet.markercluster": "^1.5.5",
"@unhead/vue": "^1.11.14",
"axios": "^1.7.7",
@@ -19,7 +21,6 @@
"vuetify": "^3.7.2"
},
"devDependencies": {
"@mdi/font": "^7.4.47",
"@tsconfig/node20": "^20.1.4",
"@types/leaflet": "^1.9.15",
"@types/node": "^20.14.5",
@@ -520,19 +521,51 @@
"node": ">=18"
}
},
"node_modules/@iconify-vue/ic": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@iconify-vue/ic/-/ic-1.0.1.tgz",
"integrity": "sha512-Lbe8rstZhBx8LSgsaTFaui+rGZT1Rt0rXraKFivPsXB3E7orpIaucoMUEb8Wgy9l1KgGNm2bUVe2s5wC4qnr7A==",
"license": "Apache-2.0",
"dependencies": {
"@iconify/css-vue": "^1.0.0"
}
},
"node_modules/@iconify-vue/mdi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@iconify-vue/mdi/-/mdi-1.0.1.tgz",
"integrity": "sha512-A6RLb4YmyjH9xbTtX74YOco8p1QpGlBwcvIAZOCHvVyCoMIIcbvVOnMak3dhqoFaepjkqhiCnqoRn8yPu/FMhg==",
"license": "Apache-2.0",
"dependencies": {
"@iconify/css-vue": "^1.0.0"
}
},
"node_modules/@iconify/css-vue": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@iconify/css-vue/-/css-vue-1.0.1.tgz",
"integrity": "sha512-rGkUIToUFUfP1zIYrY8A1pWUcadGxbMgAsUyI4PmK6BgxTO5Sajw1cbl6gqMi/D26S6LSjEG/Q+7O7gcLJthaA==",
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"vue": ">=3"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"license": "MIT"
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",

View File

@@ -12,6 +12,8 @@
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@iconify-vue/ic": "^1.0.1",
"@iconify-vue/mdi": "^1.0.1",
"@types/leaflet.markercluster": "^1.5.5",
"@unhead/vue": "^1.11.14",
"axios": "^1.7.7",
@@ -23,7 +25,6 @@
"vuetify": "^3.7.2"
},
"devDependencies": {
"@mdi/font": "^7.4.47",
"@tsconfig/node20": "^20.1.4",
"@types/leaflet": "^1.9.15",
"@types/node": "^20.14.5",

View File

@@ -5,6 +5,22 @@ import { useTheme } from 'vuetify';
import DiscordWarningDialog from '@/components/DiscordWarningDialog.vue';
import { useDiscordIntercept } from '@/composables/useDiscordIntercept';
// Icons
import HamburgerIcon from '@iconify-vue/mdi/menu';
import HomeIcon from '@iconify-vue/mdi/home';
import MapIcon from '@iconify-vue/mdi/map';
import SchoolIcon from '@iconify-vue/mdi/school';
import ShoppingIcon from '@iconify-vue/mdi/shopping-cart';
import MapMarkerPlusIcon from '@iconify-vue/mdi/map-marker-plus';
import FileDocumentIcon from '@iconify-vue/mdi/file-document';
import AccountVoiceIcon from '@iconify-vue/mdi/account-voice';
import EmailOutlineIcon from '@iconify-vue/mdi/email-outline';
import GithubIcon from '@iconify-vue/mdi/github';
import HeartIcon from '@iconify-vue/mdi/heart';
import DiscordIcon from '@iconify-vue/ic/baseline-discord';
import ChevronDownIcon from '@iconify-vue/mdi/chevron-down';
import ThemeIcon from '@iconify-vue/mdi/theme-light-dark';
const theme = useTheme();
const router = useRouter();
const isDark = computed(() => theme.name.value === 'dark');
@@ -34,23 +50,23 @@ onMounted(() => {
});
const items = [
{ title: 'Home', icon: 'mdi-home', to: '/' },
{ title: 'Map', icon: 'mdi-map', to: '/map' },
{ title: 'Learn', icon: 'mdi-school', to: '/what-is-an-alpr' },
{ title: 'Store', icon: 'mdi-shopping', to: '/store' },
{ title: 'Home', icon: HomeIcon, to: '/' },
{ title: 'Map', icon: MapIcon, to: '/map' },
{ title: 'Learn', icon: SchoolIcon, to: '/what-is-an-alpr' },
{ title: 'Store', icon: ShoppingIcon, to: '/store' },
]
const contributeItems = [
{ title: 'Submit Cameras', icon: 'mdi-map-marker-plus', to: '/report' },
{ title: 'Public Records', icon: 'mdi-file-document', to: '/foia' },
{ title: 'City Council', icon: 'mdi-account-voice', to: '/council' },
{ title: 'Submit Cameras', icon: MapMarkerPlusIcon, to: '/report' },
{ title: 'Public Records', icon: FileDocumentIcon, to: '/foia' },
{ title: 'City Council', icon: AccountVoiceIcon, to: '/council' },
]
const metaItems = [
{ title: 'Discord', customIcon: '/icon-discord.svg', customIconDark: '/icon-discord-white.svg', customIconGrey: '/icon-discord-grey.svg', href: 'https://discord.gg/aV7v4R3sKT'},
{ title: 'Contact', icon: 'mdi-email-outline', to: '/contact' },
{ title: 'GitHub', icon: 'mdi-github', href: 'https://github.com/frillweeman/deflock'},
{ title: 'Donate', icon: 'mdi-heart', to: '/donate'},
{ title: 'Discord', icon: DiscordIcon, href: 'https://discord.gg/aV7v4R3sKT'},
{ title: 'Contact', icon: EmailOutlineIcon, to: '/contact' },
{ title: 'GitHub', icon: GithubIcon, href: 'https://github.com/frillweeman/deflock'},
{ title: 'Donate', icon: HeartIcon, to: '/donate'},
];
const drawer = ref(false)
@@ -79,6 +95,7 @@ watch(() => theme.global.name.value, (newTheme) => {
@click.stop="drawer = !drawer"
class="d-md-none"
aria-label="Toggle Navigation Drawer"
:icon="HamburgerIcon"
></v-app-bar-nav-icon>
<!-- Logo -->
@@ -114,7 +131,7 @@ watch(() => theme.global.name.value, (newTheme) => {
<v-btn
variant="text"
v-bind="props"
append-icon="mdi-chevron-down"
:append-icon="ChevronDownIcon"
class="mx-1"
>
Contribute
@@ -128,7 +145,9 @@ watch(() => theme.global.name.value, (newTheme) => {
link
>
<template v-slot:prepend>
<v-icon>{{ item.icon }}</v-icon>
<v-icon>
<component :is="item.icon" class="custom-icon" />
</v-icon>
</template>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
@@ -141,7 +160,7 @@ watch(() => theme.global.name.value, (newTheme) => {
<v-btn
variant="text"
v-bind="props"
append-icon="mdi-chevron-down"
:append-icon="ChevronDownIcon"
class="mx-1"
>
Get Involved
@@ -157,15 +176,9 @@ watch(() => theme.global.name.value, (newTheme) => {
link
>
<template v-slot:prepend>
<v-icon v-if="item.icon">{{ item.icon }}</v-icon>
<v-img
v-else-if="item.customIcon"
class="mr-8"
contain
width="24"
height="24"
:src="isDark ? item.customIconDark : item.customIconGrey"
/>
<v-icon>
<component :is="item.icon" />
</v-icon>
</template>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
@@ -176,8 +189,10 @@ watch(() => theme.global.name.value, (newTheme) => {
<v-spacer class="d-md-none" />
<v-btn icon>
<v-icon @click="toggleTheme" aria-label="Toggle Theme">mdi-theme-light-dark</v-icon>
<v-btn icon @click="toggleTheme" aria-label="Toggle Theme">
<v-icon>
<ThemeIcon />
</v-icon>
</v-btn>
</v-app-bar>
@@ -196,7 +211,9 @@ watch(() => theme.global.name.value, (newTheme) => {
:to="item.to"
role="option"
>
<v-icon start>{{ item.icon }}</v-icon>
<v-icon start>
<component :is="item.icon" />
</v-icon>
{{ item.title }}
</v-list-item>
</v-list>
@@ -212,7 +229,9 @@ watch(() => theme.global.name.value, (newTheme) => {
:to="item.to"
role="option"
>
<v-icon v-if="item.icon" start>{{ item.icon }}</v-icon>
<v-icon start>
<component :is="item.icon" />
</v-icon>
<span style="vertical-align: middle;">{{ item.title }}</span>
</v-list-item>
</v-list>
@@ -230,16 +249,9 @@ watch(() => theme.global.name.value, (newTheme) => {
:target="item.href ? '_blank' : undefined"
role="option"
>
<v-icon v-if="item.icon" start>{{ item.icon }}</v-icon>
<v-img
v-else-if="item.customIcon"
class="mr-2 custom-icon"
contain
width="24"
height="24"
:src="isDark ? item.customIconDark : item.customIcon"
style="vertical-align: middle;"
/>
<v-icon start>
<component :is="item.icon" />
</v-icon>
<span style="vertical-align: middle;">{{ item.title }}</span>
</v-list-item>
</v-list>
@@ -257,10 +269,3 @@ watch(() => theme.global.name.value, (newTheme) => {
/>
</v-app>
</template>
<style lang="css" scoped>
.custom-icon {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@@ -8,16 +8,22 @@
>
<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>
<v-icon size="large" class="mr-2">
<AlertCircleIcon />
</v-icon>
<h3 class="headline">Are you sure it's an ALPR?</h3>
<v-spacer v-if="$vuetify.display.mobile"></v-spacer>
<v-btn
icon
v-if="$vuetify.display.mobile"
icon="mdi-close"
variant="text"
color="on-warning"
@click="dismiss"
></v-btn>
>
<v-icon>
<CloseIcon />
</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-6 flex-grow-1 d-flex flex-column justify-center">
@@ -43,10 +49,14 @@
variant="elevated"
size="large"
to="/identify"
prepend-icon="mdi-image-search"
class="mb-3"
@click="dismiss"
>
<template #prepend>
<v-icon>
<ImageSearchIcon />
</v-icon>
</template>
View ALPR Gallery
</v-btn>
</div>
@@ -71,6 +81,10 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import AlertCircleIcon from '@iconify-vue/mdi/alert-circle';
import CloseIcon from '@iconify-vue/mdi/close';
import ImageSearchIcon from '@iconify-vue/mdi/image-search';
const show = ref(false);
onMounted(() => {

View File

@@ -1,71 +0,0 @@
<template>
<v-data-table
:headers="headers"
:items="datasets"
hide-default-footer
:sort-by="[ { key: 'state', order: 'asc' } ]"
>
<template v-slot:item.author="i: any">
<a v-if="i.item.authorUrl" :href="i.item.authorUrl" target="_blank">{{ i.item.author }}</a>
<span v-else>{{ i.item.author }}</span>
</template>
<template v-slot:item.url="i: any">
<v-btn variant="text" color="primary" :href="i.item.url" target="_blank" :disabled="!i.item.url">
<v-icon start>mdi-download</v-icon>
<span>Download ({{ i.item.type }})</span>
</v-btn>
</template>
</v-data-table>
</template>
<script setup lang="ts">
const datasets = [
{
title: 'Wi-Fi Hits',
author: 'Ryan O\'Horo',
authorUrl: 'https://www.ryanohoro.com/post/spotting-flock-safety-s-falcon-cameras',
url: 'https://deflock-clusters.s3.us-east-1.amazonaws.com/Flock-_______20240530_124303.csv',
description: 'Crowdsourced Wi-Fi hits from Flock Safety cameras',
type: 'csv',
},
{
title: 'External Battery (Bluetooth) Hits - Flock',
author: 'Ryan O\'Horo',
authorUrl: 'https://www.ryanohoro.com/post/spotting-flock-safety-s-falcon-cameras',
url: 'https://deflock-clusters.s3.us-east-1.amazonaws.com/FS+Ext+Battery_20240530_105846.csv',
description: 'Crowdsourced Bluetooth hits from Flock Safety cameras with "Flock" radio name',
type: 'csv',
},
{
title: 'External Battery (Bluetooth) Hits - Penguin',
author: 'Ryan O\'Horo',
authorUrl: 'https://www.ryanohoro.com/post/spotting-flock-safety-s-falcon-cameras',
url: 'https://deflock-clusters.s3.us-east-1.amazonaws.com/Penguin-___________20240530_111436.csv',
description: 'Crowdsourced Bluetooth hits from Flock Safety cameras with "Penguin" radio name',
type: 'csv',
},
{
title: 'Atlanta, GA ALPRs',
author: 'veroniquedra',
authorUrl: null,
url: 'https://deflock-clusters.s3.us-east-1.amazonaws.com/maximum_dots.csv',
description: 'Over 2,000 cameras in the Atlanta area',
type: 'csv',
},
{
title: 'Akron, Ohio ALPRs',
author: 'organizers of Akron Ohio',
authorUrl: null,
url: 'https://deflock-clusters.s3.us-east-1.amazonaws.com/Pigvision.csv',
description: 'Crowdsourced Bluetooth hits from Flock Safety cameras with "Penguin" radio name',
type: 'csv',
},
];
const headers = [
{ title: 'Title', value: 'title', sortable: false },
{ title: 'Author', value: 'author', sortable: false },
{ title: 'Description', value: 'description', sortable: false },
{ title: '', value: 'url', sortable: false },
];
</script>

View File

@@ -1,7 +1,9 @@
<template>
<div style="position: relative">
<v-btn v-if="showCopyButton" color="white" @click="copyToClipboard" icon variant="plain" flat class="copy-button">
<v-icon class="copy-icon-with-shadow">mdi-content-copy</v-icon>
<v-icon class="copy-icon-with-shadow">
<CopyIcon />
</v-icon>
</v-btn>
<code ref="codeContent">
<slot></slot>
@@ -13,7 +15,9 @@
variant="text"
@click="snackbarOpen = false"
>
<v-icon>mdi-close</v-icon>
<v-icon>
<CloseIcon />
</v-icon>
</v-btn>
</template>
</v-snackbar>
@@ -23,6 +27,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import CloseIcon from '@iconify-vue/mdi/close';
import CopyIcon from '@iconify-vue/mdi/content-copy';
defineProps({
showCopyButton: {
type: Boolean,

View File

@@ -10,7 +10,9 @@
<v-list density="compact" class="my-2">
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-police-badge"></v-icon>
<v-icon>
<PoliceBadgeIcon />
</v-icon>
</template>
<v-list-item-subtitle style="font-size: 1em">
@@ -28,7 +30,9 @@
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-factory"></v-icon>
<v-icon>
<FactoryIcon />
</v-icon>
</template>
<v-list-item-subtitle style="font-size: 1em">
@@ -44,7 +48,12 @@
</v-list>
<div class="text-center">
<v-btn target="_blank" size="x-small" :href="osmNodeLink(props.alpr.id)" variant="text" color="grey"><v-icon start>mdi-open-in-new</v-icon>View on OSM</v-btn>
<v-btn target="_blank" size="x-small" :href="osmNodeLink(props.alpr.id)" variant="text" color="grey">
<v-icon start>
<OpenInNewIcon />
</v-icon>
View on OSM
</v-btn>
</div>
</v-sheet>
</template>
@@ -55,6 +64,10 @@ import type { ComputedRef, PropType } from 'vue';
import type { ALPR } from '@/types';
import { VIcon, VList, VSheet, VListItem, VBtn, VImg, VListItemSubtitle, VDivider } from 'vuetify/components';
import PoliceBadgeIcon from '@iconify-vue/mdi/police-badge';
import FactoryIcon from '@iconify-vue/mdi/factory';
import OpenInNewIcon from '@iconify-vue/mdi/open-in-new';
const props = defineProps({
alpr: {
type: Object as PropType<ALPR>,

View File

@@ -3,7 +3,9 @@
<v-card class="discord-warning-card">
<v-card-text>
<div class="discord-warning-content">
<v-icon color="warning" size="28" class="mb-2">mdi-alert</v-icon>
<v-icon color="warning" size="28" class="mb-2">
<AlertIcon />
</v-icon>
<p class="mb-3 text-body-1">
<strong>You're about to join Discord</strong>
</p>
@@ -17,7 +19,9 @@
</v-card-text>
<v-card-actions class="justify-end pt-0">
<v-btn color="primary" @click="proceed" class="mr-2" rounded>
<v-icon start>mdi-arrow-right</v-icon>
<v-icon start>
<ArrowRightIcon />
</v-icon>
Proceed
</v-btn>
<v-btn variant="text" @click="cancel" rounded>
@@ -29,6 +33,9 @@
</template>
<script setup lang="ts">
import AlertIcon from '@iconify-vue/mdi/alert';
import ArrowRightIcon from '@iconify-vue/mdi/arrow-right';
const props = defineProps<{ modelValue: boolean; discordUrl: string }>();
const emit = defineEmits(['update:modelValue', 'proceed']);

View File

@@ -15,7 +15,9 @@
<v-card-text class="py-0">
<div class="d-flex align-center justify-space-between">
<span>
<v-icon size="small" class="mr-2">mdi-chart-bubble</v-icon>
<v-icon size="small" class="mr-2">
<GroupingIcon />
</v-icon>
<span class="text-caption mr-2">Grouping</span>
</span>
<v-switch
@@ -35,7 +37,9 @@
<v-card-text class="py-0">
<div class="d-flex align-center justify-space-between">
<span>
<v-icon size="small" class="mr-2">mdi-map-outline</v-icon>
<v-icon size="small" class="mr-2">
<CityBoundariesIcon />
</v-icon>
<span class="text-caption mr-2">City Boundaries</span>
</span>
<v-switch
@@ -61,7 +65,9 @@
v-if="showAutoDisabledStatus"
class="clustering-status-bar"
>
<v-icon size="small" class="mr-2">mdi-information</v-icon>
<v-icon size="small" class="mr-2">
<InformationIcon />
</v-icon>
<span class="text-caption">
Camera grouping is on for performance at this zoom level.
</span>
@@ -73,7 +79,9 @@
class="ml-2"
@click="dismissZoomWarning"
>
<v-icon size="small">mdi-close</v-icon>
<v-icon size="small">
<CloseIcon />
</v-icon>
</v-btn>
</div>
</v-slide-y-transition>
@@ -94,6 +102,11 @@ import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import { useTheme } from 'vuetify';
import GroupingIcon from '@iconify-vue/ic/baseline-bubble-chart';
import CityBoundariesIcon from '@iconify-vue/mdi/map-outline';
import InformationIcon from '@iconify-vue/mdi/information-outline';
import CloseIcon from '@iconify-vue/mdi/close';
const MARKER_COLOR = 'rgb(63,84,243)';
const CLUSTER_DISABLE_ZOOM = 16; // Clustering disabled at zoom 16 and above

View File

@@ -12,14 +12,18 @@
<v-list class="text-center">
<v-list-item class="my-4">
<v-icon size="x-large" color="primary" class="mb-2">mdi-progress-pencil</v-icon>
<v-icon size="x-large" color="primary" class="mb-2">
<ProgressIcon />
</v-icon>
<v-list-item-title class="font-weight-bold">The map is incomplete!</v-list-item-title>
<v-list-item-subtitle>
New locations are always being added.
</v-list-item-subtitle>
</v-list-item>
<v-list-item class="my-4">
<v-icon size="x-large" color="primary" class="mb-2">mdi-square-edit-outline</v-icon>
<v-icon size="x-large" color="primary" class="mb-2">
<EditIcon />
</v-icon>
<v-list-item-title class="font-weight-bold">Add missing points!</v-list-item-title>
<v-list-item-subtitle>
Know of a missing ALPR? <router-link to="/report/id">Contribute</router-link> to the map.
@@ -38,6 +42,9 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import ProgressIcon from '@iconify-vue/mdi/progress-pencil';
import EditIcon from '@iconify-vue/mdi/square-edit-outline';
const show = ref(false);
onMounted(() => {

View File

@@ -3,7 +3,7 @@
<v-col cols="12" sm="6" class="text-center">
<v-select
color="rgb(18, 151, 195)"
prepend-inner-icon="mdi-factory"
:prepend-inner-icon="CompanyIcon"
v-model="selectedBrand"
:items="alprBrands"
item-title="nickname"
@@ -22,7 +22,7 @@
max-width="100%"
class="my-4"
></v-img>
<v-btn to="/what-is-an-alpr#photos" color="#1297C3" variant="tonal" size="small"><v-icon start>mdi-image-multiple</v-icon> See All Photos</v-btn>
<v-btn to="/identify" color="#1297C3" variant="tonal" size="small"><v-icon start><ImageMultipleIcon /></v-icon> See All Photos</v-btn>
</v-col>
<v-col cols="12" sm="6">
@@ -42,7 +42,7 @@
</DFCode>
<div class="text-caption text-center mt-1">
<a href="https://www.wikidata.org/wiki/Wikidata:Main_Page" target="_blank" rel="noopener" class="text-decoration-none text-grey-darken-1">
What is WikiData? <v-icon size="x-small">mdi-open-in-new</v-icon>
What is WikiData? <v-icon size="x-small"><OpenInNewIcon /></v-icon>
</a>
</div>
</v-col>
@@ -54,6 +54,10 @@ import DFCode from '@/components/DFCode.vue';
import { ref, type Ref } from 'vue';
import type { WikidataItem } from '@/types';
import CompanyIcon from '@iconify-vue/mdi/company';
import ImageMultipleIcon from '@iconify-vue/mdi/image-multiple';
import OpenInNewIcon from '@iconify-vue/mdi/open-in-new';
const alprBrands: WikidataItem[] = [
{
name: 'Flock Safety',

View File

@@ -39,10 +39,11 @@
<template v-slot:placeholder>
<v-row class="fill-height ma-0" align="center" justify="center">
<v-icon
icon="mdi-web"
size="x-large"
color="grey-lighten-2"
></v-icon>
>
<WebIcon />
</v-icon>
</v-row>
</template>
</v-img>
@@ -50,11 +51,12 @@
<!-- Overlay with external link icon -->
<div class="project-overlay">
<v-icon
icon="mdi-open-in-new"
color="white"
size="large"
class="external-link-icon"
></v-icon>
>
<OpenInNewIcon />
</v-icon>
</div>
</div>
@@ -78,10 +80,14 @@
color="primary"
variant="tonal"
size="small"
prepend-icon="mdi-arrow-top-right"
block
@click.stop
>
<template v-slot:prepend>
<v-icon>
<ArrowTopRightIcon />
</v-icon>
</template>
Visit Site
</v-btn>
</v-card-actions>
@@ -92,6 +98,10 @@
</template>
<script setup lang="ts">
import WebIcon from '@iconify-vue/mdi/web';
import OpenInNewIcon from '@iconify-vue/mdi/open-in-new';
import ArrowTopRightIcon from '@iconify-vue/mdi/arrow-top-right';
interface SimilarProject {
name: string;
description: string;

View File

@@ -20,7 +20,9 @@
:aria-label="link.alt"
>
<v-list-item-title class="d-flex align-center">
<v-icon class="custom-icon" start :icon="link.icon" :alt="link.alt" />
<v-icon start :alt="link.alt">
<component :is="link.icon" />
</v-icon>
{{ link.title }}
</v-list-item-title>
</v-list-item>
@@ -42,8 +44,9 @@
role="listitem"
>
<v-list-item-title class="d-flex align-center justify-start">
<v-icon start v-if="link.icon" class="custom-icon" :icon="link.icon"></v-icon>
<img v-else-if="link.customIcon" class="mr-2 custom-icon" width="24" height="24" :src="isDark ? link.customIconDark : link.customIcon" :alt="link.alt" />
<v-icon start :alt="link.alt">
<component :is="link.icon" />
</v-icon>
{{ link.title }}
</v-list-item-title>
</v-list-item>
@@ -70,29 +73,36 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useTheme } from 'vuetify';
import InformationIcon from '@iconify-vue/mdi/information';
import ShieldLockIcon from '@iconify-vue/mdi/shield-lock';
import FileDocumentIcon from '@iconify-vue/mdi/file-document';
import NewspaperIcon from '@iconify-vue/mdi/newspaper';
import EmailIcon from '@iconify-vue/mdi/email';
import HeartIcon from '@iconify-vue/mdi/heart';
import GithubIcon from '@iconify-vue/mdi/github';
import DiscordIcon from '@iconify-vue/ic/baseline-discord';
const theme = useTheme();
const isDark = computed(() => theme.name.value === 'dark');
const currentYear = new Date().getFullYear();
const internalLinks = [
{ title: 'About', to: '/about', icon: 'mdi-information', alt: 'About' },
{ title: 'Privacy Policy', to: '/privacy', icon: 'mdi-shield-lock', alt: 'Privacy Policy' },
{ title: 'Terms of Service', to: '/terms', icon: 'mdi-file-document', alt: 'Terms of Service' },
{ title: 'Press', to: '/press', icon: 'mdi-newspaper', alt: 'Press' },
{ title: 'Contact', to: '/contact', icon: 'mdi-email', alt: 'Contact' },
{ title: 'About', to: '/about', icon: InformationIcon, alt: 'About' },
{ title: 'Privacy Policy', to: '/privacy', icon: ShieldLockIcon, alt: 'Privacy Policy' },
{ title: 'Terms of Service', to: '/terms', icon: FileDocumentIcon, alt: 'Terms of Service' },
{ title: 'Press', to: '/press', icon: NewspaperIcon, alt: 'Press' },
{ title: 'Contact', to: '/contact', icon: EmailIcon, alt: 'Contact' },
];
const externalLinks = [
{ title: 'Discord', href: 'https://discord.gg/aV7v4R3sKT', customIcon: '/icon-discord.svg', customIconDark: '/icon-discord-white.svg', alt: 'Discord Logo' },
{ title: 'Donate', to: '/donate', icon: 'mdi-heart', alt: 'Donate' },
{ title: 'GitHub', href: 'https://github.com/FoggedLens/deflock', icon: 'mdi-github', alt: 'GitHub Logo' },
{ title: 'Discord', href: 'https://discord.gg/aV7v4R3sKT', icon: DiscordIcon, alt: 'Discord Logo' },
{ title: 'Donate', to: '/donate', icon: HeartIcon, alt: 'Donate' },
{ title: 'GitHub', href: 'https://github.com/FoggedLens/deflock', icon: GithubIcon, alt: 'GitHub Logo' },
]
</script>
<style scoped>
.custom-icon {
opacity: var(--v-medium-emphasis-opacity);
}
.copyright p {
font-size: 0.85rem;
line-height: 0.5rem;

View File

@@ -10,7 +10,6 @@ import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader
const vuetify = createVuetify({
components,

View File

@@ -8,13 +8,16 @@
<p class="caption">... just like the license plate this guy is searching for.</p>
</div>
<v-btn color="primary" to="/"><v-icon start>mdi-home</v-icon>Home</v-btn>
<v-btn color="primary" to="/"><v-icon start>
<HomeIcon />
</v-icon>Home</v-btn>
</div>
</DefaultLayout>
</template>
<script setup lang="ts">
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import HomeIcon from '@iconify-vue/mdi/home';
</script>
<style scoped>

View File

@@ -25,11 +25,12 @@
>
<v-card-item class="text-center pa-6">
<v-icon
icon="mdi-camera"
size="48"
color="primary"
class="mb-4"
></v-icon>
>
<CameraIcon />
</v-icon>
<v-card-title class="text-h5 font-weight-bold card-title-wrap">
Report a Camera
</v-card-title>
@@ -41,7 +42,7 @@
<v-btn
color="primary"
size="large"
append-icon="mdi-arrow-right"
:append-icon="ArrowRightIcon"
>
Start Report
</v-btn>
@@ -61,11 +62,12 @@
>
<v-card-item class="text-center pa-6">
<v-icon
:icon="showContactOptions ? 'mdi-message-text' : 'mdi-message-text'"
size="48"
color="secondary"
class="mb-4"
></v-icon>
>
<MessageTextIcon />
</v-icon>
<v-card-title class="text-h5 font-weight-bold card-title-wrap">
{{ showContactOptions ? 'Get in Touch' : 'General Inquiry' }}
</v-card-title>
@@ -78,7 +80,7 @@
<v-btn
color="secondary"
size="large"
append-icon="mdi-arrow-right"
:append-icon="ArrowRightIcon"
>
Contact Options
</v-btn>
@@ -133,10 +135,13 @@
<script setup lang="ts">
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import { useTheme } from 'vuetify';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import CameraIcon from '@iconify-vue/mdi/camera';
import ArrowRightIcon from '@iconify-vue/mdi/arrow-right';
import MessageTextIcon from '@iconify-vue/mdi/message-text';
const theme = useTheme();
const isDark = computed(() => theme.name.value === 'dark');
const showContactOptions = ref(false);
</script>

View File

@@ -13,7 +13,9 @@
<v-row>
<v-col cols="12" md="10" lg="8" class="mx-auto">
<div class="text-center mb-8">
<v-icon size="64" color="var(--df-blue)" class="mb-4">mdi-account-voice</v-icon>
<v-icon size="64" color="var(--df-blue)" class="mb-4">
<AccountVoiceIcon />
</v-icon>
<h2 class="text-h4 mb-4 font-weight-bold">Your Voice Matters Locally</h2>
<p class="text-h6 text-medium-emphasis serif">
City council members rely on us to understand public opinion. Here's your step-by-step guide to effectively advocate
@@ -36,13 +38,17 @@
>
<div class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon color="primary" class="mr-3 align-self-center">mdi-comment-alert</v-icon>
<v-icon color="primary" class="mr-3 align-self-center">
<CommentAlertIcon />
</v-icon>
<div>
<h4 class="text-h6 font-weight-bold mb-1">Talking Points</h4>
<p class="text-body-2 mb-0">Common questions, arguments & responses for discussing surveillance</p>
</div>
</div>
<v-icon color="primary" class="align-self-center">mdi-open-in-new</v-icon>
<v-icon color="primary" class="align-self-center">
<OpenInNewIcon />
</v-icon>
</div>
</v-card>
</v-col>
@@ -70,40 +76,44 @@
<v-row>
<v-col cols="12" md="6">
<h4 class="text-h6 mb-3 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-calendar-plus</v-icon>
<v-icon color="primary" class="mr-2">
<CalendarPlusIcon />
</v-icon>
How to Schedule
</h4>
<v-list density="compact">
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Contact their office via phone or email</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Suggest meeting at a local coffee shop or their office</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Request just 15-20 minutes of their time</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Mention you're a constituent concerned about ALPRs</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="6">
<h4 class="text-h6 mb-3 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-lightbulb-on</v-icon>
<v-icon color="primary" class="mr-2">
<LightbulbOnIcon />
</v-icon>
Meeting Tips
</h4>
<v-list density="compact">
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Bring a brief printed summary of key points</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Share personal concerns about privacy and community impact</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Ask about their position and listen to their concerns</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Respond to their concerns with your ideas</v-list-item-title>
</v-list-item>
</v-list>
@@ -148,40 +158,44 @@
<v-row>
<v-col cols="12" md="6">
<h4 class="text-h6 mb-3 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-clock</v-icon>
<v-icon color="primary" class="mr-2">
<ClockIcon />
</v-icon>
Before the Meeting
</h4>
<v-list density="compact">
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Check meeting schedule and agenda online</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Sign up for public comment (often required)</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Prepare 2-3 minute statement (practice timing)</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Bring a copy of your statement</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="6">
<h4 class="text-h6 mb-3 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-presentation</v-icon>
<v-icon color="primary" class="mr-2">
<PresentationIcon />
</v-icon>
During the Meeting
</h4>
<v-list density="compact">
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Arrive on time or early</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>State your name and connection to the city</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Speak clearly and maintain eye contact</v-list-item-title>
</v-list-item>
<v-list-item prepend-icon="mdi-check">
<v-list-item :prepend-icon="CheckIcon">
<v-list-item-title>Stay respectful and thank council for their time</v-list-item-title>
</v-list-item>
</v-list>
@@ -197,7 +211,9 @@
<!-- Example Videos -->
<div class="mt-6">
<h4 class="text-h6 font-weight-bold mb-4 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-video</v-icon>
<v-icon color="primary" class="mr-2">
<VideoIcon />
</v-icon>
Example Speeches
</h4>
<v-row>
@@ -210,7 +226,9 @@
@click="openVideo(video.url)"
>
<v-avatar size="48" color="primary" class="mr-3 video-play-button-compact">
<v-icon size="24" color="white">mdi-play</v-icon>
<v-icon size="24" color="white">
<PlayIcon />
</v-icon>
</v-avatar>
<div class="flex-grow-1">
@@ -232,7 +250,9 @@
<v-card class="pa-6" elevation="3" rounded="lg">
<div class="d-flex align-center mb-4">
<v-avatar size="48" color="primary" class="mr-4">
<v-icon size="24" color="white">mdi-trophy</v-icon>
<v-icon size="24" color="white">
<TrophyOutlineIcon />
</v-icon>
</v-avatar>
<div>
<h3 class="text-h5 font-weight-bold mb-1">Recent Victories</h3>
@@ -254,28 +274,34 @@
>
<template v-slot:header.cityState="{ column }">
<div class="d-flex align-center text-medium-emphasis">
<v-icon icon="mdi-map-marker" size="18" class="mr-2" />
<v-icon size="18" class="mr-2">
<MapMarkerIcon />
</v-icon>
<span class="text-caption font-weight-medium">{{ column.title }}</span>
</div>
</template>
<template v-slot:header.MonthYear="{ column }">
<div class="d-flex align-center text-medium-emphasis">
<v-icon icon="mdi-calendar-month" size="18" class="mr-2" />
<v-icon size="18" class="mr-2">
<CalendarMonthIcon />
</v-icon>
<span class="text-caption font-weight-medium">{{ column.title }}</span>
</div>
</template>
<template v-slot:header.Outcome="{ column }">
<div class="d-flex align-center text-medium-emphasis">
<v-icon icon="mdi-trophy-outline" size="18" class="mr-2" />
<v-icon size="18" class="mr-2">
<TrophyOutlineIcon />
</v-icon>
<span class="text-caption font-weight-medium">{{ column.title }}</span>
</div>
</template>
<template v-slot:item.data-table-expand="{ internalItem, isExpanded, toggleExpand }">
<v-btn
:append-icon="isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down'"
:append-icon="isExpanded(internalItem) ? ChevronUpIcon : ChevronDownIcon"
:text="isExpanded(internalItem) ? 'Collapse' : 'More info'"
class="text-none"
color="medium-emphasis"
@@ -304,7 +330,9 @@
</template>
<template v-slot:item.Outcome="{ item }">
<v-icon icon="mdi-check-bold" size="18" class="mr-2" />
<v-icon size="18" class="mr-2">
<CheckBoldIcon />
</v-icon>
<span class="font-weight-bold">{{ item.Outcome }}</span>
</template>
@@ -314,7 +342,9 @@
<template v-slot:no-data>
<div class="text-center py-8">
<v-icon size="48" color="grey-lighten-1" class="mb-4">mdi-database-off</v-icon>
<v-icon size="48" color="grey-lighten-1" class="mb-4">
<DatabaseOffIcon />
</v-icon>
<div class="text-h6 text-medium-emphasis">No victories found</div>
<div class="text-body-2 text-medium-emphasis mt-2">
Check your connection and try again
@@ -324,7 +354,7 @@
color="primary"
variant="outlined"
class="mt-4"
prepend-icon="mdi-refresh"
:prepend-icon="RefreshIcon"
>
Retry
</v-btn>
@@ -345,7 +375,9 @@
<v-col cols="12" md="10" lg="8" class="mx-auto">
<v-card class="pa-6" elevation="3" rounded="lg" color="primary" variant="tonal">
<div class="text-center">
<v-icon size="64" color="primary" class="mb-4">mdi-comment-question</v-icon>
<v-icon size="64" color="primary" class="mb-4">
<CommentQuestionIcon />
</v-icon>
<h3 class="text-h4 font-weight-bold mb-4">Need Help or Have Questions?</h3>
<p class="text-h6 mb-6 serif">
Join our supportive community of activists and experienced speakers who are ready to help you succeed.
@@ -357,8 +389,12 @@
size="large"
color="primary"
class="mr-4 mb-4"
prepend-icon="mdi-discord"
>
<template #prepend>
<v-icon>
<DiscordIcon />
</v-icon>
</template>
Join #campaigning Channel
</v-btn>
</div>
@@ -374,6 +410,27 @@ import DefaultLayout from '@/layouts/DefaultLayout.vue';
import Hero from '@/components/layout/Hero.vue';
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import AccountVoiceIcon from '@iconify-vue/mdi/account-voice';
import CommentAlertIcon from '@iconify-vue/mdi/comment-alert';
import OpenInNewIcon from '@iconify-vue/mdi/open-in-new';
import CalendarPlusIcon from '@iconify-vue/mdi/calendar-plus';
import CheckIcon from '@iconify-vue/mdi/check';
import CheckBoldIcon from '@iconify-vue/mdi/check-bold';
import PresentationIcon from '@iconify-vue/mdi/presentation';
import VideoIcon from '@iconify-vue/mdi/video';
import CommentQuestionIcon from '@iconify-vue/mdi/comment-question';
import MapMarkerIcon from '@iconify-vue/mdi/map-marker';
import CalendarMonthIcon from '@iconify-vue/mdi/calendar-month';
import TrophyOutlineIcon from '@iconify-vue/mdi/trophy-outline';
import ChevronUpIcon from '@iconify-vue/mdi/chevron-up';
import ChevronDownIcon from '@iconify-vue/mdi/chevron-down';
import DatabaseOffIcon from '@iconify-vue/mdi/database-off';
import RefreshIcon from '@iconify-vue/mdi/refresh';
import LightbulbOnIcon from '@iconify-vue/mdi/lightbulb-on';
import ClockIcon from '@iconify-vue/mdi/clock';
import DiscordIcon from '@iconify-vue/ic/baseline-discord';
import PlayIcon from '@iconify-vue/mdi/play';
const sortMonthYearByDateDesc = (a: string, b: string) => {
const [aMonth, aYear] = a.split(/\s/);
const [bMonth, bYear] = b.split(/\s/);

View File

@@ -20,10 +20,14 @@
size="large"
color="black"
class="download-btn ios-btn"
prepend-icon="mdi-apple"
:href="appLinks.ios"
target="_blank"
>
<template #prepend>
<v-icon>
<AppleIcon />
</v-icon>
</template>
Download for iOS
</v-btn>
<v-btn
@@ -31,10 +35,14 @@
variant="outlined"
color="black"
class="download-btn android-btn"
prepend-icon="mdi-google-play"
:href="appLinks.android"
target="_blank"
>
<template #prepend>
<v-icon>
<GooglePlayIcon />
</v-icon>
</template>
Get on Android
</v-btn>
@@ -43,11 +51,17 @@
variant="tonal"
color="white"
to="/app/docs"
prepend-icon="mdi-book-open-variant"
class="download-btn mt-8"
>
<template #prepend>
<v-icon>
<BookOpenIcon />
</v-icon>
</template>
Read the User Guide
<v-icon icon="mdi-open-in-new" size="small" class="ml-1" />
<v-icon size="small" class="ml-1">
<OpenInNewIcon />
</v-icon>
</v-btn>
</div>
</div>
@@ -80,7 +94,9 @@
<v-card class="feature-card" elevation="4">
<v-card-text class="text-center pa-8">
<div class="feature-icon mb-6">
<v-icon :icon="feature.icon" size="48" :color="feature.color" />
<v-icon size="48" :color="feature.color">
<component :is="feature.icon" />
</v-icon>
</div>
<h3 class="feature-title mb-3">{{ feature.title }}</h3>
<p class="feature-description">{{ feature.description }}</p>
@@ -119,17 +135,6 @@
</div>
</v-col>
</v-row>
<!-- <div class="screenshots-toggle" style="text-align:center; margin-top:32px;">
<v-btn
variant="tonal"
color="primary"
@click="showAllScreenshots = !showAllScreenshots"
rounded
size="large"
>
{{ showAllScreenshots ? 'Show Less' : 'Show More Screenshots' }}
</v-btn>
</div> -->
</div>
</v-container>
</section>
@@ -169,7 +174,9 @@
>
<v-card-text class="pa-8">
<h3 class="principle-title mb-3">
<v-icon icon="mdi-lock" color="primary" class="me-3" />
<v-icon color="primary" class="me-3">
<LockIcon />
</v-icon>
{{ principle.title }}
</h3>
<p class="principle-description mb-0">
@@ -196,10 +203,14 @@
size="x-large"
color="primary"
class="cta-btn ios-cta"
prepend-icon="mdi-apple"
:href="appLinks.ios"
target="_blank"
>
<template #prepend>
<v-icon>
<AppleIcon />
</v-icon>
</template>
Download for iPhone
</v-btn>
<v-btn
@@ -207,10 +218,14 @@
variant="outlined"
color="primary"
class="cta-btn android-cta"
prepend-icon="mdi-google-play"
:href="appLinks.android"
target="_blank"
>
<template #prepend>
<v-icon>
<GooglePlayIcon />
</v-icon>
</template>
Get on Android
</v-btn>
</div>
@@ -226,11 +241,20 @@
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import { ref, computed } from 'vue';
import AppleIcon from '@iconify-vue/mdi/apple';
import GooglePlayIcon from '@iconify-vue/mdi/google-play';
import BookOpenIcon from '@iconify-vue/mdi/book-open-variant';
import OpenInNewIcon from '@iconify-vue/mdi/open-in-new';
import MapSearchIcon from '@iconify-vue/mdi/map-search';
import CameraPlusIcon from '@iconify-vue/mdi/camera-plus';
import AccountGroupIcon from '@iconify-vue/mdi/account-group';
import LockIcon from '@iconify-vue/mdi/lock';
interface Feature {
id: number;
title: string;
description: string;
icon: string;
icon: any;
color: string;
}
@@ -247,12 +271,6 @@ interface Statistic {
label: string;
}
interface PrivacyFeature {
id: number;
text: string;
icon: string;
}
interface PrivacyPrinciple {
id: number;
title: string;
@@ -270,21 +288,21 @@ const features: Feature[] = [
id: 1,
title: 'Discover ALPRs',
description: 'Find automatic license plate readers in your neighborhood with our comprehensive database.',
icon: 'mdi-map-search',
icon: MapSearchIcon,
color: 'primary'
},
{
id: 2,
title: 'Report New Cameras',
description: 'Easily report new ALPR installations you discover to help grow the community database.',
icon: 'mdi-camera-plus',
icon: CameraPlusIcon,
color: 'success'
},
{
id: 3,
title: 'Community Driven',
description: 'Join a community of privacy advocates working together to map surveillance.',
icon: 'mdi-account-group',
icon: AccountGroupIcon,
color: 'info'
},
];
@@ -330,31 +348,6 @@ const statistics: Statistic[] = [
},
];
// Privacy features
const privacyFeatures: PrivacyFeature[] = [
{
id: 1,
text: 'No personal data collection',
icon: 'mdi-check-circle'
},
{
id: 2,
text: 'No advertising or tracking',
icon: 'mdi-check-circle'
},
{
id: 3,
text: 'Open source transparency',
icon: 'mdi-check-circle'
},
{
id: 4,
text: 'Local data storage',
icon: 'mdi-check-circle'
}
];
// Privacy principles for detailed policy
const privacyPrinciples: PrivacyPrinciple[] = [
{
id: 1,

View File

@@ -22,7 +22,9 @@
>
<v-card class="toc-sidebar" elevation="1" sticky>
<v-card-title class="text-h6 py-3">
<v-icon start>mdi-format-list-bulleted</v-icon>
<v-icon start>
<ListBulletedIcon />
</v-icon>
Table of Contents
</v-card-title>
<v-divider />
@@ -109,6 +111,8 @@ import { ref, onMounted, nextTick, watch } from 'vue';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import Hero from '@/components/layout/Hero.vue';
import ListBulletedIcon from '@iconify-vue/mdi/format-list-bulleted';
interface CMSResponse {
data: {
id: number;

View File

@@ -143,10 +143,14 @@
size="x-large"
color="primary"
to="/report"
prepend-icon="mdi-map-marker-plus"
variant="elevated"
class="mr-4"
>
<template #prepend>
<v-icon>
<MapMarkerPlusIcon />
</v-icon>
</template>
Add to Map
</v-btn>
</v-card-text>
@@ -160,6 +164,8 @@
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import Hero from '@/components/layout/Hero.vue';
import MapMarkerPlusIcon from '@iconify-vue/mdi/map-marker-plus';
function openImageInNewTab(url: string) {
window.open(url, '_blank');
}

View File

@@ -20,7 +20,7 @@
<v-btn size="large" color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
Explore the Map
<v-icon end>mdi-map</v-icon>
<v-icon end><MapIcon /></v-icon>
</v-btn>
</v-col>
</v-row>
@@ -59,7 +59,7 @@
<v-col cols="12" md="4" class="text-center">
<v-card>
<v-card-title class="headline">
<v-icon x-large class="mr-2">mdi-shield-alert</v-icon>
<v-icon x-large class="mr-2"><ShieldAlertIcon /></v-icon>
Privacy Violations
</v-card-title>
<v-card-text>
@@ -70,7 +70,7 @@
<v-col cols="12" md="4" class="text-center">
<v-card>
<v-card-title class="headline">
<v-icon x-large class="mr-2">mdi-robber</v-icon>
<v-icon x-large class="mr-2"><RobberIcon /></v-icon>
Risk of Misuse
</v-card-title>
<v-card-text>
@@ -81,7 +81,7 @@
<v-col cols="12" md="4" class="text-center">
<v-card>
<v-card-title class="headline">
<v-icon x-large class="mr-2">mdi-handcuffs</v-icon>
<v-icon x-large class="mr-2"><HandcuffsIcon /></v-icon>
Limited Benefits
</v-card-title>
<v-card-text>
@@ -96,7 +96,7 @@
</p>
<v-btn class="my-4" color="rgb(18, 151, 195)" large to="/what-is-an-alpr">
<v-icon start>mdi-book-open-page-variant</v-icon>
<v-icon start><BookOpenPageVariantIcon /></v-icon>
Learn about ALPRs
</v-btn>
@@ -113,7 +113,7 @@
<v-btn class="mt-4" variant="outlined" color="rgb(18, 151, 195)" to="/what-is-an-alpr#similar">
Learn more about Flock
<v-icon end>mdi-arrow-right</v-icon>
<v-icon end><ArrowRightIcon /></v-icon>
</v-btn>
</div>
</v-container>
@@ -123,12 +123,68 @@
<h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2>
<v-btn color="white" large @click="goToMap({ withCurrentLocation: true })">
View the Map
<v-icon end>mdi-map</v-icon>
<v-icon end><MapIcon /></v-icon>
</v-btn>
</v-container>
</DefaultLayout>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import ALPRCounter from '@/components/ALPRCounter.vue';
import { useGlobalStore } from '@/stores/global';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import MapIcon from '@iconify-vue/mdi/map';
import ShieldAlertIcon from '@iconify-vue/mdi/shield-alert';
import RobberIcon from '@iconify-vue/mdi/robber';
import HandcuffsIcon from '@iconify-vue/mdi/handcuffs';
import BookOpenPageVariantIcon from '@iconify-vue/mdi/book-open-page-variant';
import ArrowRightIcon from '@iconify-vue/mdi/arrow-right';
const router = useRouter();
const { setCurrentLocation } = useGlobalStore();
interface GoToMapOptions {
withCurrentLocation?: boolean;
}
const featuredOn = [
{
name: 'Forbes',
logo: '/white-logos/forbes.svg',
url: 'https://www.forbes.com/sites/larsdaniel/2024/11/26/think-youre-not-being-watched-deflock-says-think-again/',
},
{
name: '404 Media',
logo: '/white-logos/404media.svg',
url: 'https://www.404media.co/the-open-source-project-deflock-is-mapping-license-plate-surveillance-cameras-all-over-the-world/',
},
{
name: 'LA Times',
logo: '/white-logos/latimes.svg',
url: 'https://www.latimes.com/california/story/2024-11-14/are-there-automated-license-plate-readers-in-your-city-theres-an-open-source-program-for-that',
wide: true,
}
];
async function goToMap(options: GoToMapOptions = {}) {
if (options.withCurrentLocation) {
setCurrentLocation()
.then((currentLocation) => {
const [lat, lon] = currentLocation;
router.push({ path: '/map', hash: `#map=12/${lat.toFixed(6)}/${lon.toFixed(6)}` });
})
.catch(() => {
router.push({ path: '/map' });
});
} else {
router.push({ path: '/map' });
}
}
</script>
<style>
.hero-section {
background: url('/hero.webp') no-repeat center center;
@@ -214,53 +270,4 @@
width: 100%;
text-decoration: none !important;
}
</style>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import ALPRCounter from '@/components/ALPRCounter.vue';
import { useGlobalStore } from '@/stores/global';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
const router = useRouter();
const { setCurrentLocation } = useGlobalStore();
interface GoToMapOptions {
withCurrentLocation?: boolean;
}
const featuredOn = [
{
name: 'Forbes',
logo: '/white-logos/forbes.svg',
url: 'https://www.forbes.com/sites/larsdaniel/2024/11/26/think-youre-not-being-watched-deflock-says-think-again/',
},
{
name: '404 Media',
logo: '/white-logos/404media.svg',
url: 'https://www.404media.co/the-open-source-project-deflock-is-mapping-license-plate-surveillance-cameras-all-over-the-world/',
},
{
name: 'LA Times',
logo: '/white-logos/latimes.svg',
url: 'https://www.latimes.com/california/story/2024-11-14/are-there-automated-license-plate-readers-in-your-city-theres-an-open-source-program-for-that',
wide: true,
}
];
async function goToMap(options: GoToMapOptions = {}) {
if (options.withCurrentLocation) {
setCurrentLocation()
.then((currentLocation) => {
const [lat, lon] = currentLocation;
router.push({ path: '/map', hash: `#map=12/${lat.toFixed(6)}/${lon.toFixed(6)}` });
})
.catch(() => {
router.push({ path: '/map' });
});
} else {
router.push({ path: '/map' });
}
}
</script>
</style>

View File

@@ -18,7 +18,6 @@
:density="xs ? 'compact' : 'default'"
class="map-search"
ref="searchField"
prepend-inner-icon="mdi-magnify"
placeholder="Search for a location"
single-line
variant="solo"
@@ -27,9 +26,12 @@
v-model="searchInput"
type="search"
>
<template v-slot:prepend-inner>
<v-icon><MagnifyIcon /></v-icon>
</template>
<template v-slot:append-inner>
<v-btn :disabled="!searchInput" variant="text" flat color="#0080BC" @click="onSearch">
Go<v-icon end>mdi-chevron-right</v-icon>
Go<v-icon end><ChevronRightIcon /></v-icon>
</v-btn>
</template>
</v-text-field>
@@ -38,7 +40,9 @@
<!-- CURRENT LOCATION -->
<template v-slot:bottomright>
<v-fab icon="mdi-crosshairs-gps" @click="goToUserLocation" />
<v-fab icon @click="goToUserLocation">
<v-icon><CrosshairsGpsIcon /></v-icon>
</v-fab>
</template>
</leaflet-map>
<div v-else class="loader">
@@ -64,6 +68,11 @@ import 'leaflet/dist/leaflet.css'
import LeafletMap from '@/components/LeafletMap.vue';
import NewVisitor from '@/components/NewVisitor.vue';
// Icons
import MagnifyIcon from '@iconify-vue/mdi/magnify';
import ChevronRightIcon from '@iconify-vue/mdi/chevron-right';
import CrosshairsGpsIcon from '@iconify-vue/mdi/crosshairs-gps';
const DEFAULT_ZOOM = 12;
const zoom: Ref<number> = ref(DEFAULT_ZOOM);

View File

@@ -20,9 +20,11 @@
color="blue"
variant="tonal"
to="/identify"
prepend-icon="mdi-image-search"
size="large"
>
<template #prepend>
<v-icon><ImageSearchIcon /></v-icon>
</template>
View ALPR Gallery
</v-btn>
</v-col>
@@ -60,7 +62,7 @@
size="large"
>
Download App
<v-icon icon="mdi-arrow-right" end></v-icon>
<v-icon end><ArrowRightIcon /></v-icon>
</v-btn>
</v-card-actions>
</v-card>
@@ -98,7 +100,7 @@
size="large"
>
How to Edit
<v-icon icon="mdi-arrow-right" end></v-icon>
<v-icon end><ArrowRightIcon /></v-icon>
</v-btn>
</v-card-actions>
</v-card>
@@ -111,6 +113,9 @@
<script setup lang="ts">
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import ALPRVerificationDialog from '@/components/ALPRVerificationDialog.vue';
import ImageSearchIcon from '@iconify-vue/mdi/image-search';
import ArrowRightIcon from '@iconify-vue/mdi/arrow-right';
</script>
<style scoped>

View File

@@ -13,7 +13,13 @@
Editing the Map
</h1>
<v-stepper-vertical color="rgb(18, 151, 195)" v-model="step" flat non-linear class="my-8" edit-icon="mdi-home">
<v-stepper-vertical
color="rgb(18, 151, 195)"
v-model="step"
flat
non-linear
class="my-8"
>
<template v-slot:default="{ step }: { step: any }">
<v-stepper-vertical-item
class="transparent"
@@ -22,6 +28,11 @@
value="1"
editable
>
<template v-slot:icon="{ hasCompleted, step: targetStep }">
<v-icon>
<component :is="getStepperIcon(hasCompleted, targetStep)" />
</v-icon>
</template>
<p>
<a href="https://www.openstreetmap.org/user/new" target="_blank">Sign up for an OpenStreetMap account</a> in order to submit changes.
</p>
@@ -34,6 +45,11 @@
value="2"
editable
>
<template v-slot:icon="{ hasCompleted, step: targetStep }">
<v-icon>
<component :is="getStepperIcon(hasCompleted, targetStep)" />
</v-icon>
</template>
<p>
<a href="https://www.openstreetmap.org" target="_blank">Launch OpenStreetMap</a> and search for the location of the ALPR. You can use the search bar at the top of the page to find the location.
</p>
@@ -46,6 +62,11 @@
value="3"
editable
>
<template v-slot:icon="{ hasCompleted, step: targetStep }">
<v-icon>
<component :is="getStepperIcon(hasCompleted, targetStep)" />
</v-icon>
</template>
<p>
Once you've found the location of the ALPR, click the <strong>Edit</strong> button in the top left corner of the page. This will open the OpenStreetMap editor, where you can add the ALPR to the map.
</p>
@@ -53,9 +74,14 @@
<v-alert
variant="tonal"
type="warning"
color="warning"
class="mt-16 mb-6"
>
<template #prepend>
<v-icon>
<AlertCircleIcon />
</v-icon>
</template>
<p>
Add cameras as <strong>standalone points only</strong>! Do not connect them to roads, buildings, or other objects. Place the point exactly where the camera is physically located, but keep it as an independent point on the map.
</p>
@@ -84,6 +110,11 @@
value="4"
editable
>
<template v-slot:icon="{ hasCompleted, step: targetStep }">
<v-icon>
<component :is="getStepperIcon(hasCompleted, targetStep)" />
</v-icon>
</template>
<v-img
max-width="450"
class="my-8"
@@ -110,15 +141,25 @@
value="5"
editable
>
<template v-slot:icon="{ hasCompleted, step: targetStep }">
<v-icon>
<component :is="getStepperIcon(hasCompleted, targetStep)" />
</v-icon>
</template>
<p>
Once you've added the ALPR to the map, click the <strong>Save</strong> button in the top right corner of the editor. You'll be asked to provide a brief description of your changes. Once you've submitted your changes, the ALPR will be added to OpenStreetMap.
</p>
<v-alert
variant="tonal"
type="info"
color="info"
class="my-6"
title="How Long Will It Take?"
>
<template #prepend>
<v-icon>
<AlertCircleIcon />
</v-icon>
</template>
<p>
We pull data from OpenStreetMap <i>hourly</i>, so it may take up to an hour for your changes to appear on this site. Rest assured that your changes will be reflected here soon. As we continue to scale, we hope to reduce this delay.
</p>
@@ -133,12 +174,27 @@
<script setup lang="ts">
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import Hero from '@/components/layout/Hero.vue';
import { ref, onMounted, watch } from 'vue';
import { ref, onMounted, watch, h } from 'vue';
import OSMTagSelector from '@/components/OSMTagSelector.vue';
import { VStepperVerticalItem, VStepperVertical } from 'vuetify/labs/components';
import PendingIcon from '@iconify-vue/mdi/timer-sand';
import CompleteIcon from '@iconify-vue/mdi/check';
import AlertCircleIcon from '@iconify-vue/mdi/alert-circle';
const step = ref(parseInt(localStorage.getItem('currentStep') || '1'));
// Reusable stepper icon template function
const getStepperIcon = (hasCompleted: boolean, targetStep: number) => {
if (step.value === targetStep) {
return h(PendingIcon);
} else if (hasCompleted) {
return h(CompleteIcon);
} else {
return h('span', { style: 'font-style: normal;' }, targetStep.toString());
}
};
onMounted(() => {
step.value = parseInt(localStorage.getItem('currentStep') || '1');
});

View File

@@ -42,22 +42,28 @@
v-model="selectedType"
:items="typeOptions"
label="Filter by type"
prepend-inner-icon="mdi-filter"
variant="outlined"
clearable
hide-details
class="filter-select"
>
<template v-slot:prepend-inner>
<v-icon><FilterIcon /></v-icon>
</template>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon :color="getTypeColor(item.raw.value)">{{ getTypeIcon(item.raw.value) }}</v-icon>
<v-icon :color="getTypeColor(item.raw.value)">
<component :is="getTypeIcon(item.raw.value)" />
</v-icon>
</template>
</v-list-item>
</template>
<template v-slot:selection="{ item }">
<div class="d-flex align-center">
<v-icon :color="getTypeColor(item.raw.value)" class="mr-2">{{ getTypeIcon(item.raw.value) }}</v-icon>
<v-icon :color="getTypeColor(item.raw.value)" class="mr-2">
<component :is="getTypeIcon(item.raw.value)" />
</v-icon>
<span class="text-capitalize">{{ item.raw.title }}</span>
</div>
</template>
@@ -110,12 +116,16 @@
size="small"
class="text-capitalize mb-2 font-weight-bold"
>
<v-icon start size="small">{{ getTypeIcon(printable.type) }}</v-icon>
<template v-slot:prepend>
<v-icon size="small" class="mr-2">
<component :is="getTypeIcon(printable.type)" />
</v-icon>
</template>
{{ deCamel(printable.type) }}
</v-chip>
<div class="d-flex align-center text-caption text-grey mb-3">
<v-icon size="small" class="mr-1">mdi-account</v-icon>
<v-icon size="small" class="mr-1"><AccountIcon /></v-icon>
by {{ printable.author }}
<v-tooltip text="Licensed under CC BY-NC 4.0">
<template v-slot:activator="{ props }">
@@ -125,12 +135,12 @@
class="ml-1"
color="grey"
>
mdi-creative-commons
<CreativeCommonsIcon />
</v-icon>
</template>
</v-tooltip>
<v-spacer />
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
<v-icon size="small" class="mr-1"><ClockIcon /></v-icon>
{{ formatDate(printable.date_updated) }}
</div>
@@ -145,9 +155,11 @@
variant="tonal"
color="primary"
size="small"
prepend-icon="mdi-download"
class="flex-1-1-50"
>
<template v-slot:prepend>
<v-icon><DownloadIcon /></v-icon>
</template>
<span v-if="printable.back">Front Side</span>
<span v-else>Download</span>
</v-btn>
@@ -160,9 +172,11 @@
variant="tonal"
color="secondary"
size="small"
prepend-icon="mdi-download"
class="flex-1-1-50"
>
<template v-slot:prepend>
<v-icon><DownloadIcon /></v-icon>
</template>
Back Side
</v-btn>
</div>
@@ -175,7 +189,7 @@
<!-- Empty State -->
<div v-else class="text-center py-12">
<v-icon size="64" color="grey-lighten-1">mdi-inbox-outline</v-icon>
<v-icon size="64" color="grey-lighten-1"><InboxIcon /></v-icon>
<h3 class="text-h5 mt-4 mb-2 text-grey">No printables available</h3>
<p class="text-grey">Check back later for new content!</p>
</div>
@@ -190,9 +204,11 @@
color="primary"
variant="outlined"
size="large"
prepend-icon="mdi-upload"
class="text-none"
>
<template v-slot:prepend>
<v-icon><UploadIcon /></v-icon>
</template>
Submit Your Artwork
</v-btn>
<p class="text-caption text-grey mt-2">
@@ -209,6 +225,21 @@ import type { Ref } from 'vue';
import DefaultLayout from '@/layouts/DefaultLayout.vue';
import Hero from '@/components/layout/Hero.vue';
// Icon imports
import FilterIcon from '@iconify-vue/mdi/filter';
import AccountIcon from '@iconify-vue/mdi/account';
import CreativeCommonsIcon from '@iconify-vue/mdi/creative-commons';
import ClockIcon from '@iconify-vue/mdi/clock-outline';
import DownloadIcon from '@iconify-vue/mdi/download';
import InboxIcon from '@iconify-vue/mdi/inbox-outline';
import UploadIcon from '@iconify-vue/mdi/upload';
import PostIcon from '@iconify-vue/mdi/post';
import BookIcon from '@iconify-vue/mdi/book-open-page-variant';
import SignIcon from '@iconify-vue/mdi/sign-real-estate';
import StickerIcon from '@iconify-vue/mdi/sticker-circle-outline';
import RectangleIcon from '@iconify-vue/mdi/rectangle-outline';
import FileIcon from '@iconify-vue/mdi/file';
// Types
interface Printable {
id: number;
@@ -289,15 +320,15 @@ const getTypeColor = (type: string): string => {
return colors[type] || 'grey';
};
const getTypeIcon = (type: string): string => {
const icons: Record<string, string> = {
poster: 'mdi-post',
zine: 'mdi-book-open-page-variant',
yardSign: 'mdi-sign-real-estate',
sticker: 'mdi-sticker-circle-outline',
bumperSticker: 'mdi-rectangle-outline',
const getTypeIcon = (type: string) => {
const icons: Record<string, any> = {
poster: PostIcon,
zine: BookIcon,
yardSign: SignIcon,
sticker: StickerIcon,
bumperSticker: RectangleIcon,
};
return icons[type] || 'mdi-file';
return icons[type] || FileIcon;
};
const formatDate = (dateString: string): string => {

View File

@@ -39,12 +39,12 @@
<v-divider class="my-12" />
<h2 class="mb-8">
<v-icon class="mr-2" color="primary">mdi-camera-outline</v-icon>
<v-icon class="mr-2" color="primary"><CameraOutlineIcon /></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>
<v-icon left class="mr-2"><ImageSearchIcon /></v-icon>
View ALPR Images
</v-btn>
</div>
@@ -76,6 +76,9 @@ import Hero from '@/components/layout/Hero.vue';
import Dangers from '@/components/Dangers.vue';
import FAQ from '@/components/FAQ.vue';
import SimilarProjects from '@/components/SimilarProjects.vue';
import CameraOutlineIcon from '@iconify-vue/mdi/camera-outline';
import ImageSearchIcon from '@iconify-vue/mdi/image-search';
</script>
<style scoped>