Merge branch 'better-icons'

This commit is contained in:
Will Freeman
2024-12-27 20:24:35 -07:00
42 changed files with 1806 additions and 1208 deletions

View File

@@ -29,6 +29,7 @@ class GithubClient(implicit val system: ActorSystem, implicit val executionConte
| login
| name
| avatarUrl
| url
| }
| }
| }

437
webapp/package-lock.json generated
View File

@@ -8,9 +8,12 @@
"name": "deflock",
"version": "0.0.0",
"dependencies": {
"@types/leaflet.markercluster": "^1.5.5",
"@unhead/vue": "^1.11.14",
"axios": "^1.7.7",
"countup.js": "^2.8.0",
"leaflet.markercluster": "^1.5.3",
"pinia": "^2.3.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vuetify": "^3.7.2"
@@ -18,9 +21,9 @@
"devDependencies": {
"@mdi/font": "^7.4.47",
"@tsconfig/node20": "^20.1.4",
"@types/leaflet": "^1.9.15",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/tsconfig": "^0.5.1",
"leaflet": "^1.9.4",
"npm-run-all2": "^6.2.0",
@@ -46,11 +49,11 @@
}
},
"node_modules/@babel/parser": {
"version": "7.26.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz",
"integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"dependencies": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.26.3"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -60,9 +63,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -670,6 +673,27 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/geojson": {
"version": "7946.0.15",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz",
"integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA=="
},
"node_modules/@types/leaflet": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.15.tgz",
"integrity": "sha512-7UuggAuAs+mva66gtf2OTB1nEhzU/9JED93TIaOEgvFMvG/dIGQaukHE7izHo1Zd+Ko1L4ETUw7TBc8yUxevpg==",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/leaflet.markercluster": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.5.tgz",
"integrity": "sha512-TkWOhSHDM1ANxmLi+uK0PjsVcjIKBr8CLV2WoF16dIdeFmC0Cj5P5axkI3C1Xsi4+ht6EU8+BfEbbqEF9icPrg==",
"dependencies": {
"@types/leaflet": "*"
}
},
"node_modules/@types/node": {
"version": "20.16.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.6.tgz",
@@ -771,68 +795,50 @@
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue-leaflet/vue-leaflet": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz",
"integrity": "sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==",
"dev": true,
"dependencies": {
"vue": "^3.2.25"
},
"peerDependencies": {
"@types/leaflet": "^1.5.7",
"leaflet": "^1.6.0"
},
"peerDependenciesMeta": {
"@types/leaflet": {
"optional": true
}
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.8.tgz",
"integrity": "sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.8",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.8.tgz",
"integrity": "sha512-GUNHWvoDSbSa5ZSHT9SnV5WkStWfzJwwTd6NMGzilOE/HM5j+9EB9zGXdtu/fCNEmctBqMs6C9SvVPpVPuk1Eg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dependencies": {
"@vue/compiler-core": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.8.tgz",
"integrity": "sha512-taYpngQtSysrvO9GULaOSwcG5q821zCoIQBtQQSx7Uf7DxpR6CIHR90toPr9QfDD2mqHQPCSgoWBvJu0yV9zjg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.8",
"@vue/compiler-dom": "3.5.8",
"@vue/compiler-ssr": "3.5.8",
"@vue/shared": "3.5.8",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.47",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.8.tgz",
"integrity": "sha512-W96PtryNsNG9u0ZnN5Q5j27Z/feGrFV6zy9q5tzJVyJaLiwYxvC0ek4IXClZygyhjm+XKM7WD9pdKi/wIRVC/Q==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dependencies": {
"@vue/compiler-dom": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -875,49 +881,49 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.8.tgz",
"integrity": "sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dependencies": {
"@vue/shared": "3.5.8"
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.8.tgz",
"integrity": "sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dependencies": {
"@vue/reactivity": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.8.tgz",
"integrity": "sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dependencies": {
"@vue/reactivity": "3.5.8",
"@vue/runtime-core": "3.5.8",
"@vue/shared": "3.5.8",
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.8.tgz",
"integrity": "sha512-7AmC9/mEeV9mmXNVyUIm1a1AjUhyeeGNbkLh39J00E7iPeGks8OGRB5blJiMmvqSh8SkaS7jkLWSpXtxUCeagA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dependencies": {
"@vue/compiler-ssr": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"vue": "3.5.8"
"vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.8.tgz",
"integrity": "sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A=="
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
},
"node_modules/@vue/tsconfig": {
"version": "0.5.1",
@@ -1159,13 +1165,20 @@
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"node_modules/leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"peerDependencies": {
"leaflet": "^1.3.1"
}
},
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
@@ -1286,9 +1299,9 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/pidtree": {
"version": "0.6.0",
@@ -1302,10 +1315,31 @@
"node": ">=0.10"
}
},
"node_modules/pinia": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@@ -1322,7 +1356,7 @@
],
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -1531,15 +1565,15 @@
"dev": true
},
"node_modules/vue": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.8.tgz",
"integrity": "sha512-hvuvuCy51nP/1fSRvrrIqTLSvrSyz2Pq+KQ8S8SXCxTWVE0nMaOnSDnSOxV1eYmGfvK7mqiwvd1C59CEEz7dAQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dependencies": {
"@vue/compiler-dom": "3.5.8",
"@vue/compiler-sfc": "3.5.8",
"@vue/runtime-dom": "3.5.8",
"@vue/server-renderer": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"
@@ -1550,6 +1584,31 @@
}
}
},
"node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-router": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",
@@ -1646,17 +1705,17 @@
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="
},
"@babel/parser": {
"version": "7.26.1",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz",
"integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"requires": {
"@babel/types": "^7.26.0"
"@babel/types": "^7.26.3"
}
},
"@babel/types": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"requires": {
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
@@ -1958,6 +2017,27 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"@types/geojson": {
"version": "7946.0.15",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz",
"integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA=="
},
"@types/leaflet": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.15.tgz",
"integrity": "sha512-7UuggAuAs+mva66gtf2OTB1nEhzU/9JED93TIaOEgvFMvG/dIGQaukHE7izHo1Zd+Ko1L4ETUw7TBc8yUxevpg==",
"requires": {
"@types/geojson": "*"
}
},
"@types/leaflet.markercluster": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.5.tgz",
"integrity": "sha512-TkWOhSHDM1ANxmLi+uK0PjsVcjIKBr8CLV2WoF16dIdeFmC0Cj5P5axkI3C1Xsi4+ht6EU8+BfEbbqEF9icPrg==",
"requires": {
"@types/leaflet": "*"
}
},
"@types/node": {
"version": "20.16.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.6.tgz",
@@ -2038,59 +2118,50 @@
"vscode-uri": "^3.0.8"
}
},
"@vue-leaflet/vue-leaflet": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz",
"integrity": "sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==",
"dev": true,
"requires": {
"vue": "^3.2.25"
}
},
"@vue/compiler-core": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.8.tgz",
"integrity": "sha512-Uzlxp91EPjfbpeO5KtC0KnXPkuTfGsNDeaKQJxQN718uz+RqDYarEf7UhQJGK+ZYloD2taUbHTI2J4WrUaZQNA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"requires": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.8",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"@vue/compiler-dom": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.8.tgz",
"integrity": "sha512-GUNHWvoDSbSa5ZSHT9SnV5WkStWfzJwwTd6NMGzilOE/HM5j+9EB9zGXdtu/fCNEmctBqMs6C9SvVPpVPuk1Eg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"requires": {
"@vue/compiler-core": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/compiler-sfc": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.8.tgz",
"integrity": "sha512-taYpngQtSysrvO9GULaOSwcG5q821zCoIQBtQQSx7Uf7DxpR6CIHR90toPr9QfDD2mqHQPCSgoWBvJu0yV9zjg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"requires": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.8",
"@vue/compiler-dom": "3.5.8",
"@vue/compiler-ssr": "3.5.8",
"@vue/shared": "3.5.8",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.47",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"@vue/compiler-ssr": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.8.tgz",
"integrity": "sha512-W96PtryNsNG9u0ZnN5Q5j27Z/feGrFV6zy9q5tzJVyJaLiwYxvC0ek4IXClZygyhjm+XKM7WD9pdKi/wIRVC/Q==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"requires": {
"@vue/compiler-dom": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/compiler-vue2": {
@@ -2125,46 +2196,46 @@
}
},
"@vue/reactivity": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.8.tgz",
"integrity": "sha512-mlgUyFHLCUZcAYkqvzYnlBRCh0t5ZQfLYit7nukn1GR96gc48Bp4B7OIcSfVSvlG1k3BPfD+p22gi1t2n9tsXg==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"requires": {
"@vue/shared": "3.5.8"
"@vue/shared": "3.5.13"
}
},
"@vue/runtime-core": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.8.tgz",
"integrity": "sha512-fJuPelh64agZ8vKkZgp5iCkPaEqFJsYzxLk9vSC0X3G8ppknclNDr61gDc45yBGTaN5Xqc1qZWU3/NoaBMHcjQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"requires": {
"@vue/reactivity": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/runtime-dom": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.8.tgz",
"integrity": "sha512-DpAUz+PKjTZPUOB6zJgkxVI3GuYc2iWZiNeeHQUw53kdrparSTG6HeXUrYDjaam8dVsCdvQxDz6ZWxnyjccUjQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"requires": {
"@vue/reactivity": "3.5.8",
"@vue/runtime-core": "3.5.8",
"@vue/shared": "3.5.8",
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"@vue/server-renderer": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.8.tgz",
"integrity": "sha512-7AmC9/mEeV9mmXNVyUIm1a1AjUhyeeGNbkLh39J00E7iPeGks8OGRB5blJiMmvqSh8SkaS7jkLWSpXtxUCeagA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"requires": {
"@vue/compiler-ssr": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/shared": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.8.tgz",
"integrity": "sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A=="
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="
},
"@vue/tsconfig": {
"version": "0.5.1",
@@ -2348,13 +2419,18 @@
"leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA=="
},
"leaflet.markercluster": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
"requires": {}
},
"magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"requires": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
@@ -2432,9 +2508,9 @@
"dev": true
},
"picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"pidtree": {
"version": "0.6.0",
@@ -2442,13 +2518,22 @@
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true
},
"pinia": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
"integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
"requires": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
}
},
"postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"requires": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
}
},
@@ -2567,17 +2652,23 @@
"dev": true
},
"vue": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.8.tgz",
"integrity": "sha512-hvuvuCy51nP/1fSRvrrIqTLSvrSyz2Pq+KQ8S8SXCxTWVE0nMaOnSDnSOxV1eYmGfvK7mqiwvd1C59CEEz7dAQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"requires": {
"@vue/compiler-dom": "3.5.8",
"@vue/compiler-sfc": "3.5.8",
"@vue/runtime-dom": "3.5.8",
"@vue/server-renderer": "3.5.8",
"@vue/shared": "3.5.8"
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"requires": {}
},
"vue-router": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",

View File

@@ -12,8 +12,11 @@
},
"dependencies": {
"@unhead/vue": "^1.11.14",
"@types/leaflet.markercluster": "^1.5.5",
"axios": "^1.7.7",
"countup.js": "^2.8.0",
"leaflet.markercluster": "^1.5.3",
"pinia": "^2.3.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vuetify": "^3.7.2"
@@ -21,9 +24,9 @@
"devDependencies": {
"@mdi/font": "^7.4.47",
"@tsconfig/node20": "^20.1.4",
"@types/leaflet": "^1.9.15",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/tsconfig": "^0.5.1",
"leaflet": "^1.9.4",
"npm-run-all2": "^6.2.0",

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 118.89 37.86">
<path style="fill:#000000;" d="M25.6,.73h13.52l-5,22.98h4.68v6.55h-6.12l-1.49,6.86h-7.03l1.49-6.86H9.47v-9.67L25.6,.73ZM15.38,22.78v.94h11.71L31.35,4.16h-.96L15.38,22.78Z"></path>
<path style="fill:#000000;" d="M44.92,14.87c1.1-5.13,2.94-8.89,5.51-11.28s6.11-3.59,10.62-3.59c1.88,0,3.55,.29,5,.86,1.46,.57,2.68,1.37,3.67,2.39s1.75,2.23,2.26,3.61c.51,1.39,.77,2.88,.77,4.47,0,.59-.03,1.18-.08,1.77-.05,.59-.15,1.18-.29,1.77l-1.76,8.11c-1.1,5.13-2.94,8.89-5.51,11.28-2.57,2.39-6.11,3.59-10.62,3.59-1.88,0-3.55-.29-5-.86s-2.68-1.37-3.67-2.39c-.99-1.02-1.75-2.22-2.26-3.59-.51-1.37-.77-2.87-.77-4.5,0-.59,.03-1.18,.08-1.77,.05-.59,.15-1.18,.29-1.77l1.76-8.11Zm10.22,16.74c1.21,0,2.27-.22,3.19-.65,.92-.43,1.73-1.01,2.42-1.74,.69-.73,1.26-1.59,1.7-2.6,.44-1,.79-2.08,1.04-3.22l1.97-8.94c.11-.55,.19-1.03,.24-1.43,.05-.4,.08-.79,.08-1.17,0-1.77-.47-3.15-1.41-4.13-.94-.99-2.26-1.48-3.97-1.48-1.21,0-2.27,.22-3.19,.65-.92,.43-1.73,1.01-2.42,1.74-.69,.73-1.26,1.59-1.7,2.6-.44,1.01-.79,2.08-1.04,3.22l-1.97,8.94c-.21,.94-.32,1.8-.32,2.6,0,1.77,.47,3.15,1.41,4.13s2.26,1.48,3.97,1.48Z"></path>
<path style="fill:#000000;" d="M90.76,.73h13.52l-5,22.98h4.68v6.55h-6.12l-1.49,6.86h-7.03l1.49-6.86h-16.18v-9.67L90.76,.73Zm-10.22,22.05v.94h11.71l4.26-19.55h-.96l-15.01,18.62Z"></path>
<path style="fill:#91fc4e;" d="M1.07,31.42c-.58,.04-1.07-.41-1.07-.99v-.47c0-.23,.07-.46,.22-.64l1.07-1.31c.16-.19,.24-.43,.22-.68l-.27-5.39c0-.5,.36-.93,.86-1,9.83-1.36,104.88-12.17,115.62-13.39,.55-.06,1.03,.33,1.09,.88l.07,.68c.04,.42-.18,.82-.56,1.01l-1.51,.73c-.15,.07-.26,.21-.29,.37l-.8,4.31c-.08,.42-.41,.75-.84,.81C105.82,17.59,12.17,30.56,1.07,31.42Z"></path>
<path style="fill:#a8fd73;" d="M35.59,16.96c-2.48,.29-4.89,.57-7.21,.84l-1.29,5.92H15.38v-.94l3.08-3.82c-3.2,.38-6.05,.71-8.48,1.01l-.51,.63v9.67h1.88c7.04-.87,16.75-2.14,27.45-3.56v-2.99h-4.68l1.47-6.76Zm59.62-6.84l-2.01,9.22c2.58-.35,5.01-.69,7.24-1l1.97-9.04c-2.24,.25-4.65,.53-7.2,.82Zm-20.58,10.48v1.28c2.54-.35,5.04-.69,7.46-1.02l8.21-10.18c-2.69,.31-5.5,.63-8.39,.96l-7.27,8.96Zm-31.46,2.39c-.14,.59-.24,1.18-.29,1.77-.04,.47-.06,.95-.06,1.42,2.31-.31,4.65-.62,7.01-.94,.05-.58,.12-1.18,.27-1.83l1.83-8.33c-2.43,.28-4.84,.56-7.22,.83l-1.53,7.08Zm22.3-8.53l-1.97,8.93c2.44-.33,4.86-.66,7.26-.99l1.63-7.53c.14-.59,.24-1.18,.29-1.77,.01-.14,0-.28,.02-.42-2.33,.27-4.69,.54-7.06,.81-.05,.29-.1,.6-.17,.96Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1150 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Artboard1" x="0" y="0" width="1150" height="300" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g>
<g transform="matrix(344.475,0,0,344.475,30.1181,267.042)">
<path d="M0.377,-0.658L0.377,-0.655C0.421,-0.629 0.415,-0.593 0.415,-0.547L0.415,-0.415C0.373,-0.452 0.317,-0.473 0.261,-0.473C0.124,-0.473 0.024,-0.364 0.024,-0.229C0.024,-0.08 0.131,0.013 0.277,0.013C0.295,0.013 0.312,0.013 0.329,0.008L0.388,-0.082C0.361,-0.065 0.334,-0.053 0.302,-0.053C0.197,-0.053 0.125,-0.142 0.125,-0.243C0.125,-0.334 0.19,-0.407 0.27,-0.407C0.323,-0.407 0.374,-0.383 0.399,-0.335C0.418,-0.298 0.415,-0.254 0.415,-0.214L0.415,-0L0.544,-0L0.544,-0.003C0.5,-0.027 0.506,-0.064 0.506,-0.11L0.506,-0.674L0.503,-0.674C0.492,-0.658 0.468,-0.658 0.445,-0.658L0.377,-0.658Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
<g transform="matrix(344.475,0,0,344.475,229.914,267.042)">
<path d="M0.5,-0.246C0.504,-0.375 0.411,-0.473 0.275,-0.473C0.126,-0.473 0.025,-0.372 0.025,-0.233C0.025,-0.094 0.142,0.013 0.312,0.013C0.359,0.013 0.407,0.006 0.45,-0.012L0.5,-0.106L0.497,-0.106C0.451,-0.07 0.393,-0.053 0.333,-0.053C0.22,-0.053 0.135,-0.124 0.133,-0.246L0.5,-0.246ZM0.137,-0.304C0.149,-0.367 0.199,-0.407 0.266,-0.407C0.338,-0.407 0.384,-0.374 0.395,-0.304L0.137,-0.304Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
<g transform="matrix(344.475,0,0,344.475,409.04,267.042)">
<path d="M0.023,-0.394L0.071,-0.394L0.071,-0.11C0.071,-0.064 0.077,-0.027 0.033,-0.003L0.033,-0L0.2,-0L0.2,-0.003C0.156,-0.028 0.162,-0.064 0.162,-0.11L0.162,-0.394L0.264,-0.394C0.276,-0.394 0.291,-0.391 0.295,-0.38L0.298,-0.38L0.298,-0.46L0.162,-0.46C0.162,-0.56 0.157,-0.608 0.249,-0.608C0.278,-0.608 0.308,-0.603 0.333,-0.59L0.333,-0.11C0.333,-0.064 0.339,-0.027 0.295,-0.003L0.295,-0L0.462,-0L0.462,-0.003C0.418,-0.027 0.424,-0.064 0.424,-0.11L0.424,-0.674L0.421,-0.674C0.411,-0.663 0.394,-0.656 0.378,-0.656C0.347,-0.656 0.319,-0.674 0.266,-0.674C0.206,-0.674 0.148,-0.654 0.107,-0.608C0.068,-0.564 0.071,-0.525 0.071,-0.46L0.023,-0.394Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
<g transform="matrix(344.475,0,0,344.475,581.278,267.042)">
<path d="M0.276,0.013C0.417,0.013 0.537,-0.091 0.537,-0.235C0.537,-0.303 0.506,-0.369 0.455,-0.414C0.407,-0.456 0.352,-0.473 0.288,-0.473C0.144,-0.473 0.023,-0.376 0.023,-0.226C0.023,-0.084 0.139,0.013 0.276,0.013ZM0.281,-0.053C0.179,-0.053 0.124,-0.152 0.124,-0.244C0.124,-0.334 0.184,-0.407 0.277,-0.407C0.384,-0.407 0.436,-0.311 0.436,-0.214C0.436,-0.124 0.373,-0.053 0.281,-0.053Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
<g transform="matrix(344.475,0,0,344.475,774.184,267.042)">
<path d="M0.415,-0.461C0.38,-0.469 0.343,-0.473 0.307,-0.473C0.156,-0.473 0.022,-0.39 0.022,-0.218C0.022,-0.088 0.142,0.013 0.296,0.013C0.34,0.013 0.386,0.009 0.428,-0.007L0.48,-0.102L0.477,-0.102C0.438,-0.073 0.382,-0.053 0.331,-0.053C0.22,-0.053 0.123,-0.129 0.123,-0.244C0.123,-0.339 0.193,-0.407 0.29,-0.407C0.335,-0.407 0.383,-0.391 0.412,-0.358L0.415,-0.358L0.415,-0.461Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
<g transform="matrix(344.475,0,0,344.475,932.642,267.042)">
<path d="M0.029,-0.658L0.029,-0.655C0.072,-0.63 0.066,-0.593 0.066,-0.547L0.066,-0.111C0.066,-0.065 0.072,-0.028 0.029,-0.003L0.029,-0L0.196,-0L0.196,-0.003C0.151,-0.028 0.157,-0.065 0.157,-0.111L0.157,-0.674L0.154,-0.674C0.141,-0.659 0.117,-0.658 0.095,-0.658L0.029,-0.658ZM0.324,-0.056C0.343,-0.029 0.368,-0 0.426,-0L0.504,-0C0.459,-0.028 0.429,-0.071 0.398,-0.112L0.276,-0.267L0.443,-0.46L0.291,-0.46L0.291,-0.457C0.301,-0.451 0.31,-0.442 0.31,-0.428C0.31,-0.403 0.274,-0.365 0.259,-0.348L0.176,-0.257L0.324,-0.056Z" style="fill:rgb(104,104,104);fill-rule:nonzero;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path fill="#ffffff" d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 41.625 10.769531 C 37.644531 7.566406 31.347656 7.023438 31.078125 7.003906 C 30.660156 6.96875 30.261719 7.203125 30.089844 7.589844 C 30.074219 7.613281 29.9375 7.929688 29.785156 8.421875 C 32.417969 8.867188 35.652344 9.761719 38.578125 11.578125 C 39.046875 11.867188 39.191406 12.484375 38.902344 12.953125 C 38.710938 13.261719 38.386719 13.429688 38.050781 13.429688 C 37.871094 13.429688 37.6875 13.378906 37.523438 13.277344 C 32.492188 10.15625 26.210938 10 25 10 C 23.789063 10 17.503906 10.15625 12.476563 13.277344 C 12.007813 13.570313 11.390625 13.425781 11.101563 12.957031 C 10.808594 12.484375 10.953125 11.871094 11.421875 11.578125 C 14.347656 9.765625 17.582031 8.867188 20.214844 8.425781 C 20.0625 7.929688 19.925781 7.617188 19.914063 7.589844 C 19.738281 7.203125 19.34375 6.960938 18.921875 7.003906 C 18.652344 7.023438 12.355469 7.566406 8.320313 10.8125 C 6.214844 12.761719 2 24.152344 2 34 C 2 34.175781 2.046875 34.34375 2.132813 34.496094 C 5.039063 39.605469 12.972656 40.941406 14.78125 41 C 14.789063 41 14.800781 41 14.8125 41 C 15.132813 41 15.433594 40.847656 15.621094 40.589844 L 17.449219 38.074219 C 12.515625 36.800781 9.996094 34.636719 9.851563 34.507813 C 9.4375 34.144531 9.398438 33.511719 9.765625 33.097656 C 10.128906 32.683594 10.761719 32.644531 11.175781 33.007813 C 11.234375 33.0625 15.875 37 25 37 C 34.140625 37 38.78125 33.046875 38.828125 33.007813 C 39.242188 32.648438 39.871094 32.683594 40.238281 33.101563 C 40.601563 33.515625 40.5625 34.144531 40.148438 34.507813 C 40.003906 34.636719 37.484375 36.800781 32.550781 38.074219 L 34.378906 40.589844 C 34.566406 40.847656 34.867188 41 35.1875 41 C 35.199219 41 35.210938 41 35.21875 41 C 37.027344 40.941406 44.960938 39.605469 47.867188 34.496094 C 47.953125 34.34375 48 34.175781 48 34 C 48 24.152344 43.785156 12.761719 41.625 10.769531 Z M 18.5 30 C 16.566406 30 15 28.210938 15 26 C 15 23.789063 16.566406 22 18.5 22 C 20.433594 22 22 23.789063 22 26 C 22 28.210938 20.433594 30 18.5 30 Z M 31.5 30 C 29.566406 30 28 28.210938 28 26 C 28 23.789063 29.566406 22 31.5 22 C 33.433594 22 35 23.789063 35 26 C 35 28.210938 33.433594 30 31.5 30 Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461Z" style="fill:rgb(87,87,87);fill-opacity:0.46;"/>
<path d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461ZM217.92,200.242C228.694,192.652 241.831,188.195 256,188.195C270.169,188.195 283.306,192.652 294.08,200.242C294.08,200.242 392.803,48.4 392.803,48.4C352.363,26.364 305.694,13.806 256,13.806C206.306,13.806 159.637,26.364 119.197,48.4L217.92,200.242Z" style="fill:rgb(137,135,135);"/>
<g transform="matrix(0.906623,0,0,0.906623,23.9045,22.3271)">
<circle cx="256" cy="256" r="57.821" style="fill:rgb(63,84,243);fill-opacity:0.41;"/>
<path d="M256,174.25C301.119,174.25 337.75,210.881 337.75,256C337.75,301.119 301.119,337.75 256,337.75C210.881,337.75 174.25,301.119 174.25,256C174.25,210.881 210.881,174.25 256,174.25ZM256,198.179C224.088,198.179 198.179,224.088 198.179,256C198.179,287.912 224.088,313.821 256,313.821C287.912,313.821 313.821,287.912 313.821,256C313.821,224.088 287.912,198.179 256,198.179Z" style="fill:rgb(63,84,243);"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 118.89 37.86">
<path style="fill:#ffffff;" d="M25.6,.73h13.52l-5,22.98h4.68v6.55h-6.12l-1.49,6.86h-7.03l1.49-6.86H9.47v-9.67L25.6,.73ZM15.38,22.78v.94h11.71L31.35,4.16h-.96L15.38,22.78Z"></path>
<path style="fill:#ffffff;" d="M44.92,14.87c1.1-5.13,2.94-8.89,5.51-11.28s6.11-3.59,10.62-3.59c1.88,0,3.55,.29,5,.86,1.46,.57,2.68,1.37,3.67,2.39s1.75,2.23,2.26,3.61c.51,1.39,.77,2.88,.77,4.47,0,.59-.03,1.18-.08,1.77-.05,.59-.15,1.18-.29,1.77l-1.76,8.11c-1.1,5.13-2.94,8.89-5.51,11.28-2.57,2.39-6.11,3.59-10.62,3.59-1.88,0-3.55-.29-5-.86s-2.68-1.37-3.67-2.39c-.99-1.02-1.75-2.22-2.26-3.59-.51-1.37-.77-2.87-.77-4.5,0-.59,.03-1.18,.08-1.77,.05-.59,.15-1.18,.29-1.77l1.76-8.11Zm10.22,16.74c1.21,0,2.27-.22,3.19-.65,.92-.43,1.73-1.01,2.42-1.74,.69-.73,1.26-1.59,1.7-2.6,.44-1,.79-2.08,1.04-3.22l1.97-8.94c.11-.55,.19-1.03,.24-1.43,.05-.4,.08-.79,.08-1.17,0-1.77-.47-3.15-1.41-4.13-.94-.99-2.26-1.48-3.97-1.48-1.21,0-2.27,.22-3.19,.65-.92,.43-1.73,1.01-2.42,1.74-.69,.73-1.26,1.59-1.7,2.6-.44,1.01-.79,2.08-1.04,3.22l-1.97,8.94c-.21,.94-.32,1.8-.32,2.6,0,1.77,.47,3.15,1.41,4.13s2.26,1.48,3.97,1.48Z"></path>
<path style="fill:#ffffff;" d="M90.76,.73h13.52l-5,22.98h4.68v6.55h-6.12l-1.49,6.86h-7.03l1.49-6.86h-16.18v-9.67L90.76,.73Zm-10.22,22.05v.94h11.71l4.26-19.55h-.96l-15.01,18.62Z"></path>
<path style="fill:#ffffff;" d="M1.07,31.42c-.58,.04-1.07-.41-1.07-.99v-.47c0-.23,.07-.46,.22-.64l1.07-1.31c.16-.19,.24-.43,.22-.68l-.27-5.39c0-.5,.36-.93,.86-1,9.83-1.36,104.88-12.17,115.62-13.39,.55-.06,1.03,.33,1.09,.88l.07,.68c.04,.42-.18,.82-.56,1.01l-1.51,.73c-.15,.07-.26,.21-.29,.37l-.8,4.31c-.08,.42-.41,.75-.84,.81C105.82,17.59,12.17,30.56,1.07,31.42Z"></path>
<path style="fill:#ffffff;" d="M35.59,16.96c-2.48,.29-4.89,.57-7.21,.84l-1.29,5.92H15.38v-.94l3.08-3.82c-3.2,.38-6.05,.71-8.48,1.01l-.51,.63v9.67h1.88c7.04-.87,16.75-2.14,27.45-3.56v-2.99h-4.68l1.47-6.76Zm59.62-6.84l-2.01,9.22c2.58-.35,5.01-.69,7.24-1l1.97-9.04c-2.24,.25-4.65,.53-7.2,.82Zm-20.58,10.48v1.28c2.54-.35,5.04-.69,7.46-1.02l8.21-10.18c-2.69,.31-5.5,.63-8.39,.96l-7.27,8.96Zm-31.46,2.39c-.14,.59-.24,1.18-.29,1.77-.04,.47-.06,.95-.06,1.42,2.31-.31,4.65-.62,7.01-.94,.05-.58,.12-1.18,.27-1.83l1.83-8.33c-2.43,.28-4.84,.56-7.22,.83l-1.53,7.08Zm22.3-8.53l-1.97,8.93c2.44-.33,4.86-.66,7.26-.99l1.63-7.53c.14-.59,.24-1.18,.29-1.77,.01-.14,0-.28,.02-.42-2.33,.27-4.69,.54-7.06,.81-.05,.29-.1,.6-.17,.96Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 54">
<path fill="#ffffff" d="M113.3 18.2c0-5.8.1-11.2.4-16.2L98.4 4.9v1.4l1.5.2c1.1.1 1.8.5 2.2 1.1.4.7.7 1.7.9 3.2.2 2.9.4 9.5.3 19.9 0 10.3-.1 16.8-.3 19.3 5.5 1.2 9.8 1.7 13 1.7 6 0 10.7-1.7 14.1-5.2 3.4-3.4 5.2-8.2 5.2-14.1 0-4.7-1.3-8.6-3.9-11.7-2.6-3.1-5.9-4.6-9.8-4.6-2.6 0-5.3.7-8.3 2.1zm.3 30.8c-.2-3.2-.4-12.8-.4-28.5.9-.3 2.1-.5 3.6-.5 2.4 0 4.3 1.2 5.7 3.7 1.4 2.5 2.1 5.5 2.1 9.3 0 4.7-.8 8.5-2.4 11.7-1.6 3.1-3.6 4.7-6.1 4.7-.8-.2-1.6-.3-2.5-.4zM41 3H1v2l2.1.2c1.6.3 2.7.9 3.4 1.8.7 1 1.1 2.6 1.2 4.8.8 10.8.8 20.9 0 30.2-.2 2.2-.6 3.8-1.2 4.8-.7 1-1.8 1.6-3.4 1.8l-2.1.3v2h25.8v-2l-2.7-.2a4.9 4.9 0 0 1-3.4-1.8c-.7-1-1.1-2.6-1.2-4.8-.3-4-.5-8.6-.5-13.7l5.4.1c2.9.1 4.9 2.3 5.9 6.7h2V18.9h-2c-1 4.3-2.9 6.5-5.9 6.6l-5.4.1c0-9 .2-15.4.5-19.3h7.9c5.6 0 9.4 3.6 11.6 10.8l2.4-.7L41 3zm-4.7 30.8c0 5.2 1.5 9.5 4.4 12.9 2.9 3.4 7.2 5 12.6 5s9.8-1.7 13-5.2c3.2-3.4 4.7-7.7 4.7-12.9s-1.5-9.5-4.4-12.9c-2.9-3.4-7.2-5-12.6-5s-9.8 1.7-13 5.2c-3.2 3.4-4.7 7.7-4.7 12.9zm22.3-11.4c1.2 2.9 1.7 6.7 1.7 11.3 0 10.6-2.2 15.8-6.5 15.8-2.2 0-3.9-1.5-5.1-4.5-1.2-3-1.7-6.8-1.7-11.3C47 23.2 49.2 18 53.5 18c2.2-.1 3.9 1.4 5.1 4.4zm84.5 24.3c3.3 3.3 7.5 5 12.5 5 3.1 0 5.8-.6 8.2-1.9 2.4-1.2 4.3-2.7 5.6-4.5l-1-1.2c-2.2 1.7-4.7 2.5-7.6 2.5-4 0-7.1-1.3-9.2-4-2.2-2.7-3.2-6.1-3-10.5H170c0-4.8-1.2-8.7-3.7-11.8-2.5-3-6-4.5-10.5-4.5-5.6 0-9.9 1.8-13 5.3-3.1 3.5-4.6 7.8-4.6 12.9 0 5.2 1.6 9.4 4.9 12.7zm7.4-25.1c1.1-2.4 2.5-3.6 4.4-3.6 3 0 4.5 3.8 4.5 11.5l-10.6.2c.1-3 .6-5.7 1.7-8.1zm46.4-4c-2.7-1.2-6.1-1.9-10.2-1.9-4.2 0-7.5 1.1-10 3.2s-3.8 4.7-3.8 7.8c0 2.7.8 4.8 2.3 6.3 1.5 1.5 3.9 2.8 7 3.9 2.8 1 4.8 2 5.8 2.9 1 1 1.6 2.1 1.6 3.6 0 1.4-.5 2.7-1.6 3.7-1 1.1-2.4 1.6-4.2 1.6-4.4 0-7.7-3.2-10-9.6l-1.7.5.4 10c3.6 1.4 7.6 2.1 12 2.1 4.6 0 8.1-1 10.7-3.1 2.6-2 3.9-4.9 3.9-8.5 0-2.4-.6-4.4-1.9-5.9-1.3-1.5-3.4-2.8-6.4-4-3.3-1.2-5.6-2.3-6.8-3.3-1.2-1-1.8-2.2-1.8-3.7s.4-2.7 1.3-3.7 2-1.4 3.4-1.4c4 0 6.9 2.9 8.7 8.6l1.7-.5-.4-8.6zm-96.2-.9c-1.4-.7-2.9-1-4.6-1-1.7 0-3.4.7-5.3 2.1-1.9 1.4-3.3 3.3-4.4 5.9l.1-8-15.2 3v1.4l1.5.1c1.9.2 3 1.7 3.2 4.4.6 6.2.6 12.8 0 19.8-.2 2.7-1.3 4.1-3.2 4.4l-1.5.2v1.9h21.2V49l-2.7-.2c-1.9-.2-3-1.7-3.2-4.4-.6-5.8-.7-12-.2-18.4.6-1 1.9-1.6 3.9-1.8 2-.2 4.3.4 6.7 1.8l3.7-9.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { RouterView, useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { useTheme } from 'vuetify';
const theme = useTheme();
const router = useRouter();
const isDark = computed(() => theme.name.value === 'dark');
function toggleTheme() {
const newTheme = theme.global.name.value === 'dark' ? 'light' : 'dark';
@@ -23,7 +24,7 @@ const items = [
]
const metaItems = [
{ title: 'Discord', icon: 'mdi-chat-processing-outline', href: 'https://discord.gg/aV7v4R3sKT'},
{ title: 'Discord', customIcon: '/icon-discord.svg', customIconDark: '/icon-discord-white.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'},
@@ -34,11 +35,9 @@ watch(() => theme.global.name.value, (newTheme) => {
const root = document.documentElement;
if (newTheme === 'dark') {
root.style.setProperty('--df-background-color', 'rgb(33, 33, 33)');
root.style.setProperty('--df-page-background-color', 'unset');
root.style.setProperty('--df-text-color', '#ccc');
} else {
root.style.setProperty('--df-background-color', 'white');
root.style.setProperty('--df-page-background-color', '#f5f5f5');
root.style.setProperty('--df-text-color', 'black');
}
});
@@ -58,9 +57,9 @@ watch(() => theme.global.name.value, (newTheme) => {
<v-spacer></v-spacer>
<v-btn icon>
<!-- <v-btn icon>
<v-icon @click="toggleTheme">mdi-theme-light-dark</v-icon>
</v-btn>
</v-btn> -->
</v-app-bar>
<v-navigation-drawer
@@ -86,7 +85,11 @@ watch(() => theme.global.name.value, (newTheme) => {
:to="item.to"
:href="item.href"
:target="{ '_blank': item.href }"
><v-icon start>{{ item.icon }}</v-icon>{{ item.title }}</v-list-item>
>
<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;" />
<span style="vertical-align: middle;">{{ item.title }}</span>
</v-list-item>
</v-list>
</v-navigation-drawer>
@@ -95,3 +98,10 @@ watch(() => theme.global.name.value, (newTheme) => {
</v-main>
</v-app>
</template>
<style lang="css" scoped>
.custom-icon {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@@ -1,17 +1,18 @@
a {
font-weight: bold;
color: rgb(18, 151, 195);
color: var(--df-blue);
text-decoration: none;
}
a:hover {
/* underline only simple <a> elements */
a:not([class]):hover {
text-decoration: underline;
}
:root {
--df-background-color: white;
--df-text-color: #ccc;
--df-page-background-color: #f5f5f5;
--df-blue: rgb(18, 151, 195);
}
.leaflet-popup-content-wrapper, .leaflet-popup-tip, .leaflet-bar a {
@@ -30,3 +31,8 @@ p {
.serif {
font-family: "PT Serif", serif;
}
/* Prevent Scrolling Horizontally */
html, body {
overflow-x: hidden;
}

View File

@@ -0,0 +1,26 @@
/* Typography rules */
body {
line-height: 1.6;
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: bold;
}
p {
margin: 0.8em 0;
}
ul, ol {
margin: 1em 0;
padding-left: 1.5em;
}
ul li, ol li {
margin: 0.5em 0;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="counter">
<span ref="counterEl" class="font-weight-bold">0</span>
<span :class="{ mobile: isMobile }" ref="counterEl" class="font-weight-bold">0</span>
<span class="caption">&nbsp;ALPRs Reported Worldwide</span>
<div :class="{ 'fade-in': showFinalAnimation }" class="subheading fade-text">and rapidly growing!</div>
</div>
@@ -8,6 +8,7 @@
<script setup lang="ts">
import { onMounted, ref, watch, type Ref } from 'vue';
import { useDisplay } from 'vuetify'
import { getALPRCounts } from '@/services/apiService';
import { CountUp } from 'countup.js';
@@ -30,6 +31,7 @@ const counts: Ref<Counts> = ref({
worldwide: undefined,
});
const showFinalAnimation = ref(false);
const { xs: isMobile } = useDisplay();
onMounted(() => {
getALPRCounts().then((response) => {
@@ -74,4 +76,9 @@ watch(counts, (newCounts: Counts) => {
.fade-in {
opacity: 1;
}
.mobile {
display: block;
font-size: 1.2em;
}
</style>

View File

@@ -43,6 +43,11 @@ code {
border-radius: 0.25rem;
display: block;
margin-top: 0.5rem;
overflow-x: scroll;
}
code {
white-space: nowrap;
}
.copy-button {

View File

@@ -1,75 +0,0 @@
<template>
<l-circle-marker :lat-lng="[alpr.lat, alpr.lon]" :radius="7" :color="markerColor">
<l-popup>
<DFMapPopup :alpr="alpr" />
</l-popup>
</l-circle-marker>
<l-polygon
:lat-lngs="directionIndicatorPolygonCoordinates"
:options="{ color: 'red' }"
v-if="showFov && hasDirection"
>
<!-- TODO: use the same popup -->
<l-popup>
<DFMapPopup :alpr="alpr" />
</l-popup>
</l-polygon>
</template>
<script setup lang="ts">
import { LCircleMarker, LPolygon, LPopup } from '@vue-leaflet/vue-leaflet';
import DFMapPopup from '@/components/DFMapPopup.vue';
import type { ALPR } from '@/types';
import type { PropType } from 'vue';
import { computed, defineProps } from 'vue';
const props = defineProps({
alpr: {
type: Object as PropType<ALPR>,
required: true
},
showFov: {
type: Boolean,
default: false
}
});
const markerColor = computed(() => {
if (props.alpr.tags.brand === 'Avigilon') {
return '#ff5722';
}
return '#3f54f3';
});
const hasDirection = computed(() => props.alpr.tags.direction !== undefined);
const directionIndicatorPolygonCoordinates = computed(() => {
if (!hasDirection.value) {
console.warn('ALPR does not have direction tag');
return [];
}
const { lat, lon } = props.alpr;
const direction = parseInt(props.alpr.tags.direction);
const fov = 30; // Field of view in degrees
const distance = 0.0004; // Distance for the triangle points
const toRadians = (degrees: number) => degrees * (Math.PI / 180);
const pointL = {
lat: lat + distance * Math.cos(toRadians(direction - fov / 2)),
lon: lon + distance * Math.sin(toRadians(direction - fov / 2))
};
const pointR = {
lat: lat + distance * Math.cos(toRadians(direction + fov / 2)),
lon: lon + distance * Math.sin(toRadians(direction + fov / 2))
};
return [
[lat, lon],
[pointL.lat, pointL.lon],
[pointR.lat, pointR.lon]
];
});
</script>

View File

@@ -5,13 +5,13 @@
<v-icon start>mdi-face-recognition</v-icon> <b>Face Recognition</b>
</v-list-item>
<v-list-item>
<v-icon start>mdi-car</v-icon> <b>License Plate</b>
<v-icon start>mdi-cctv</v-icon> <b>License Plate Reader</b>
</v-list-item>
<v-list-item v-if="isFaceRecognition">
<v-icon start>mdi-adjust</v-icon> <b>Omnidirectional</b>
</v-list-item>
<v-list-item v-else>
<v-icon start>mdi-cctv</v-icon> <b>Directional {{ alpr.tags.direction ? `(${degreesToCardinal(parseInt(alpr.tags.direction))})` : '' }}</b>
<v-icon start>mdi-compass-outline</v-icon> <b>{{ cardinalDirection }}</b>
</v-list-item>
<v-list-item>
<v-icon start>mdi-domain</v-icon> <b>
@@ -23,31 +23,29 @@
</span>
<span v-else>
Unknown Manufacturer
<!-- <v-btn :href="osmNodeUrl" variant="text" flat size="x-small" color="warning" target="_blank">
<v-icon start size="small">mdi-pencil</v-icon>Fix
</v-btn> -->
</span>
</b>
</v-list-item>
<v-list-item>
<v-icon start>mdi-police-badge</v-icon> <b>
<v-icon start>mdi-account-tie</v-icon> <b>
<span v-if="alpr.tags.operator">
{{ alpr.tags.operator }}
</span>
<span v-else>
Unknown Operator
<!-- <v-btn :href="osmNodeUrl" variant="text" flat size="x-small" color="warning" target="_blank">
<v-icon start size="small">mdi-pencil</v-icon>Fix
</v-btn> -->
</span>
</b>
</v-list-item>
</v-list>
<div class="text-center text-grey-darken-1">
node/{{ alpr.id }}
<v-tooltip open-delay="500" text="OSM Node ID" location="bottom">
<template #activator="{ props }">
<span style="cursor: default" v-bind="props">node/{{ alpr.id }}</span>
</template>
</v-tooltip>
</div>
<!-- <v-data-table density="compact" hide-default-header hide-default-footer disable-sort :items="kvTags" /> -->
</v-sheet>
</template>
@@ -55,6 +53,7 @@
import { defineProps, computed } from 'vue';
import type { PropType } from 'vue';
import type { ALPR } from '@/types';
import { VIcon, VList, VSheet, VListItem, VTooltip } from 'vuetify/components';
const props = defineProps({
alpr: {
@@ -63,23 +62,14 @@ const props = defineProps({
}
});
const valueTransformations: { [key: string]: (value: string) => string } = {
direction: (value: string) => `${degreesToCardinal(parseInt(value))} ${value}º`
};
const whitelistedTags = ['brand', 'camera:mount', 'camera:type', 'direction', 'operator'];
const isFaceRecognition = computed(() => props.alpr.tags.brand === 'Avigilon');
const osmNodeUrl = computed(() => `/node/${props.alpr.id}`);
const kvTags = computed(() => {
return Object.entries(props.alpr.tags)
.filter(([key]) => whitelistedTags.includes(key))
.map(([key, value]) => ({ key, value: valueTransformations[key]?.(value) ?? value }));
});
const cardinalDirection = computed(() =>
props.alpr.tags.direction === undefined ? 'Unknown Direction' : degreesToCardinal(parseInt(props.alpr.tags.direction))
);
function degreesToCardinal(degrees: number): string {
const cardinals = ['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest'];
return cardinals[Math.round(degrees / 45) % 8];
return 'Faces ' + cardinals[Math.round(degrees / 45) % 8];
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<v-expansion-panels multiple>
<v-expansion-panels multiple :model-value :readonly="showAll">
<v-expansion-panel>
<v-expansion-panel-title class="font-weight-bold">
ALPRs Do Not Reduce Crime
@@ -19,7 +19,7 @@
What research does exist regarding the ability of ALPRs to reduce crime is inconclusive at best:
</p>
<quoted-source source-url="https://example.com/study" attribution-text="Journal of Experimental Criminology">
<quoted-source source-url="https://link.springer.com/article/10.1007/s11292-011-9133-9" attribution-text="Journal of Experimental Criminology">
Our findings indicate that, when small numbers of LPR patrols are used in crime hot spots in the way we have tested them here, they do not seem to generate either a general or offense-specific deterrent effect.
</quoted-source>
@@ -156,6 +156,16 @@
<script setup lang="ts">
import QuotedSource from '@/components/QuotedSource.vue';
import { computed } from 'vue';
const props = defineProps({
showAll: {
type: Boolean,
default: false,
}
});
const modelValue = computed(() => props.showAll ? [0,1,2,3,4,5] : []);
</script>
<style scoped>

View File

@@ -0,0 +1,295 @@
<template>
<div id="map">
<div class="topleft">
<slot name="topleft"></slot>
</div>
<div class="bottomright">
<slot name="bottomright"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, h, createApp, watch, ref, type PropType, type Ref } from 'vue';
import L, { type LatLngExpression, type FeatureGroup, type MarkerClusterGroup, type Marker, type CircleMarker } from 'leaflet';
import type { ALPR } from '@/types';
import DFMapPopup from './DFMapPopup.vue';
import { createVuetify } from 'vuetify'
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
const MARKER_COLOR = 'rgb(63,84,243)';
// Internal State Management
const markerMap = new Map<string, Marker | CircleMarker>();
const isInternalUpdate = ref(false);
const props = defineProps({
center: {
type: Object,
required: true,
},
zoom: {
type: Number,
required: true,
},
alprs: {
type: Array as PropType<ALPR[]>,
default: () => [],
},
currentLocation: {
type: Object as PropType<[number, number] | null>,
default: null,
},
});
const emit = defineEmits(['update:center', 'update:zoom', 'update:bounds']);
// Map instance and layers
let map: L.Map;
let circlesLayer: FeatureGroup;
let clusterLayer: MarkerClusterGroup;
let currentLocationLayer: FeatureGroup;
// Marker Creation Utilities
function createSVGMarker(alpr: ALPR): string {
const orientationDegrees = alpr.tags.direction;
const fovPath = `
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461Z" style="fill:rgb(87,87,87);fill-opacity:0.46;"/>
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461ZM217.92,200.242C228.694,192.652 241.831,188.195 256,188.195C270.169,188.195 283.306,192.652 294.08,200.242C294.08,200.242 392.803,48.4 392.803,48.4C352.363,26.364 305.694,13.806 256,13.806C206.306,13.806 159.637,26.364 119.197,48.4L217.92,200.242Z" style="fill:rgb(137,135,135);"/>
`;
return `
<svg style="transform:rotate(${orientationDegrees}deg)" class="svgMarker" width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
${orientationDegrees ? fovPath : ''}
<g transform="matrix(0.906623,0,0,0.906623,23.9045,22.3271)">
<circle class="someSVGpath" cx="256" cy="256" r="57.821" style="fill:${MARKER_COLOR};fill-opacity:0.41;"/>
<path class="someSVGpath" d="M256,174.25C301.119,174.25 337.75,210.881 337.75,256C337.75,301.119 301.119,337.75 256,337.75C210.881,337.75 174.25,301.119 174.25,256C174.25,210.881 210.881,174.25 256,174.25ZM256,198.179C224.088,198.179 198.179,224.088 198.179,256C198.179,287.912 224.088,313.821 256,313.821C287.912,313.821 313.821,287.912 313.821,256C313.821,224.088 287.912,198.179 256,198.179Z" style="fill:${MARKER_COLOR};"/>
</g>
</svg>
`;
}
function createMarker(alpr: ALPR): Marker | CircleMarker {
if (hasPlottableOrientation(alpr.tags.direction)) {
const icon = L.divIcon({
className: 'leaflet-data-marker',
html: createSVGMarker(alpr),
iconSize: [60, 60],
iconAnchor: [30, 30],
popupAnchor: [0, 0],
});
return L.marker([alpr.lat, alpr.lon], { icon });
}
return L.circleMarker([alpr.lat, alpr.lon], {
fill: true,
fillColor: MARKER_COLOR,
fillOpacity: 0.6,
stroke: true,
color: MARKER_COLOR,
opacity: 1,
radius: 8,
weight: 3,
});
}
function bindPopup(marker: L.CircleMarker | L.Marker, alpr: ALPR): L.CircleMarker | L.Marker {
marker.bindPopup('');
marker.on('popupopen', (e: any) => {
const popupContent = document.createElement('div');
createApp({
render() {
return h(DFMapPopup, {
alpr: {
id: alpr.id,
lat: alpr.lat,
lon: alpr.lon,
tags: alpr.tags,
type: alpr.type,
}
});
}
}).use(createVuetify()).mount(popupContent);
e.popup.setContent(popupContent);
});
return marker;
}
function hasPlottableOrientation(orientationDegrees: string) {
// OSM tags are strings, and some have multiple values (e.g. '0;90')
return orientationDegrees && !isNaN(parseInt(orientationDegrees));
}
// Map State Management
function initializeMap() {
map = L.map('map', {
zoomControl: false,
maxZoom: 18, // max for OSM tiles
minZoom: 3, // don't overload the browser
}).setView(props.center, props.zoom);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
clusterLayer = L.markerClusterGroup({
chunkedLoading: true,
disableClusteringAtZoom: 16,
removeOutsideVisibleBounds: true,
maxClusterRadius: 60,
spiderfyOnEveryZoom: false,
spiderfyOnMaxZoom: false,
});
circlesLayer = L.featureGroup();
currentLocationLayer = L.featureGroup();
map.addLayer(clusterLayer);
registerMapEvents();
if (props.alprs.length) {
updateMarkers(props.alprs);
} else {
emit('update:bounds', map.getBounds());
}
}
function updateMarkers(newAlprs: ALPR[]): void {
const currentIds = new Set(markerMap.keys());
const nonexistingAlprs = newAlprs.filter(alpr => !currentIds.has(alpr.id));
// Add markers
for (const alpr of nonexistingAlprs) {
if (!currentIds.has(alpr.id)) {
// Add new marker
const marker = createMarker(alpr);
bindPopup(marker, alpr);
markerMap.set(alpr.id, marker);
circlesLayer.addLayer(marker);
}
}
// Update cluster layer
clusterLayer.clearLayers();
clusterLayer.addLayer(circlesLayer);
}
function updateCurrentLocation(): void {
currentLocationLayer.clearLayers();
if (props.currentLocation) {
const marker = L.circleMarker([props.currentLocation[0], props.currentLocation[1]], {
radius: 10,
color: '#ffffff',
fillColor: '#007bff',
fillOpacity: 1,
weight: 4
}).bindPopup('Current Location');
currentLocationLayer.addLayer(marker);
map.addLayer(currentLocationLayer);
}
}
// Lifecycle Hooks
onMounted(() => {
initializeMap();
// Watch for prop changes
watch(() => props.center, (newCenter: any) => {
if (!isInternalUpdate.value) {
isInternalUpdate.value = true;
map.setView(newCenter, map.getZoom(), { animate: false });
setTimeout(() => {
isInternalUpdate.value = false;
}, 0);
}
});
watch(() => props.zoom, (newZoom: number) => {
if (!isInternalUpdate.value) {
isInternalUpdate.value = true;
map.setZoom(newZoom);
setTimeout(() => {
isInternalUpdate.value = false;
}, 0);
}
});
watch(() => props.alprs, (newAlprs) => {
updateMarkers(newAlprs);
}, { deep: true });
watch(() => props.currentLocation, () => {
updateCurrentLocation();
});
});
onBeforeUnmount(() => {
map?.remove();
});
function registerMapEvents() {
map.on('moveend', () => {
if (!isInternalUpdate.value) {
emit('update:center', map.getCenter());
emit('update:zoom', map.getZoom());
emit('update:bounds', map.getBounds());
}
});
}
</script>
<style scoped>
@import 'leaflet/dist/leaflet.css';
@import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
@import 'leaflet.markercluster/dist/MarkerCluster.css';
#map {
height: calc(100dvh - 64px);
margin-top: 64px;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
.topleft {
position: absolute;
top: 10px;
left: 10px;
z-index: 1000;
}
.bottomright {
position: absolute;
bottom: 50px; /* hack */
right: 60px; /* hack */
z-index: 1000;
}
</style>
<style> /* (Global) */
/* Disables clicks on the main wrappers */
.leaflet-marker-icon.leaflet-interactive:not(.marker-cluster), .svgMarker {
pointer-events: none;
cursor: default;
}
.svgMarker {
pointer-events: none;
cursor: default;
}
/* Enables clicks only on actual SVG path */
.someSVGpath {
pointer-events: all;
cursor: pointer;
}
</style>

View File

@@ -1,92 +0,0 @@
<template>
<v-dialog :fullscreen="smAndDown" v-model="show" max-width="900">
<v-card>
<v-card-title class="text-center py-4 font-weight-bold">
<span class="headline">Welcome to DeFlock</span>
</v-card-title>
<p class="mx-8">
DeFlock is a tool to help you learn about Automated License Plate Readers (ALPRs) in your area. Here's how it works:
</p>
<v-container>
<v-row>
<v-col cols="12" sm="6">
<v-card flat class="pa-4">
<v-row class="align-center">
<v-col>
<v-img width="140" src="/step1.png" />
</v-col>
<v-col>
<h4 class="no-small">
Each Circle represents an Automated License Plate Reader.
</h4>
</v-col>
</v-row>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card flat class="pa-4">
<v-row class="align-center">
<v-col>
<v-img width="140" src="/step2.png" />
</v-col>
<v-col >
<h4 class="no-small">
Zoom in to see which direction each ALPR is facing.
</h4>
</v-col>
</v-row>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card flat class="pa-4">
<v-row class="align-center">
<v-col>
<v-img width="140" src="/step3.png" />
</v-col>
<v-col>
<h4 class="no-small">
Please check our list of <a href="/operators">Known Operators</a> and report missing ALPRs near you.
</h4>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
</v-container>
<div class="text-center mx-4 mb-2">Map data from <a href="https://openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>. By using this site, you agree to our <router-link to="/legal">Terms of Service</router-link>.</div>
<v-card-actions>
<v-btn class="w-100" size="x-large" color="primary" variant="elevated" @click="acknowledge">Got it</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useDisplay } from 'vuetify';
const { smAndDown } = useDisplay();
const show = ref(false);
onMounted(() => {
if (!localStorage.getItem('acknowledged')) {
show.value = true;
}
});
function acknowledge() {
show.value = false;
localStorage.setItem('acknowledged', 'true');
}
</script>
<style scoped>
.no-small {
min-width: 160px;
}
</style>

View File

@@ -1,54 +1,49 @@
<template>
<v-row style="align-items: center; margin-top: 1.25rem;">
<v-col cols="12" sm="6">
<h2 class="text-center mb-4">Choose Brand</h2>
<v-row>
<v-col v-for="brand in alprBrands" :key="brand.wikidata" cols="6">
<v-card
@click="selectedBrand = brand"
class="text-center"
:class="{ selected: selectedBrand.wikidata === brand.wikidata }"
:image="brand.exampleImage"
min-height="180"
>
<v-card-title class="overlay">{{ brand.nickname }}</v-card-title>
</v-card>
</v-col>
</v-row>
</v-col>
<v-row class="align-center justify-center my-4">
<v-col cols="12" sm="6">
<v-select
color="rgb(18, 151, 195)"
prepend-inner-icon="mdi-factory"
v-model="selectedBrand"
:items="alprBrands"
item-title="nickname"
return-object
label="Choose a Manufacturer"
variant="outlined"
flat
hide-details
></v-select>
<v-img
:aspect-ratio="3/2"
cover
v-if="selectedBrand"
:src="selectedBrand.exampleImage"
:alt="selectedBrand.nickname"
max-width="100%"
class="my-4"
></v-img>
</v-col>
<v-col cols="12" sm="6">
<h3 class="text-center serif">Tags to Copy</h3>
<DFCode>
man_made=surveillance<br>
surveillance:type=ALPR<br>
camera:mount=pole<br>
camera:type=fixed<br>
surveillance=public<br>
surveillance:zone=traffic<br>
manufacturer=<span class="highlight">{{ selectedBrand.name }}</span><br>
manufacturer:wikidata=<span class="highlight">{{ selectedBrand.wikidata }}</span><br>
</DFCode>
<v-col cols="12" sm="6">
<h3 class="text-center">{{ selectedBrand.nickname }}</h3>
<DFCode>
man_made=surveillance<br>
surveillance:type=ALPR<br>
camera:mount=pole<br>
camera:type=fixed<br>
surveillance=public<br>
surveillance:zone=traffic<br>
manufacturer=<span class="highlight">{{ selectedBrand.name }}</span><br>
manufacturer:wikidata=<span class="highlight">{{ selectedBrand.wikidata }}</span><br>
</DFCode>
<h5 class="text-center mt-4">and if operator is known</h5>
<DFCode>
operator=<span class="highlight">(Police Dept/Owner)</span><br>
operator:wikidata=<span class="highlight">(WikiData ID)</span><br>
</DFCode>
<v-alert
v-if="selectedBrand.nickname === 'Other'"
class="mt-4"
variant="tonal"
type="warning"
icon="mdi-information"
title="Missing a brand?"
>
Please let us know by <a href="/contact">contacting us</a> or by <a href="https://github.com/frillweeman/deflock" target="_blank">contributing</a> to the project.
</v-alert>
</v-col>
</v-row>
<h5 class="text-center mt-4 serif">and if operator is known</h5>
<DFCode>
operator=<span class="highlight">(Police Dept/Owner)</span><br>
operator:wikidata=<span class="highlight">(WikiData ID)</span><br>
</DFCode>
</v-col>
</v-row>
</template>
<script setup lang="ts">
@@ -65,22 +60,22 @@ const alprBrands: WikidataItem[] = [
},
{
name: 'Motorola Solutions',
nickname: 'Motorola',
nickname: 'Motorola/Vigilant',
wikidata: 'Q634815',
exampleImage: '/vigilant-1.jpg',
},
{
name: 'Leonardo',
nickname: 'Leonardo/ELSAG',
wikidata: 'Q910379',
exampleImage: '/elsag.webp',
},
{
name: 'Neology, Inc.',
nickname: 'Neology',
wikidata: 'Q130958232',
exampleImage: '/neology-1.jpg',
},
{
name: 'Leonardo',
nickname: 'Leonardo',
wikidata: 'Q910379',
exampleImage: '/elsag.webp',
},
{
name: '(brand goes here)',
nickname: 'Other',

View File

@@ -0,0 +1,96 @@
<template>
<v-footer>
<v-container>
<v-row align-items="center" justify="center">
<v-col cols="12" class="mt-4">
<v-img height="30" contain src="/deflock-logo-grey.svg" />
</v-col>
<!-- Internal Links -->
<v-col cols="7" sm="3">
<v-list density="compact">
<v-list-subheader>Info</v-list-subheader>
<v-list-item
v-for="link in internalLinks"
:key="link.title"
link
:to="link.to"
slim
>
<v-list-item-title class="d-flex align-center">
<v-icon class="custom-icon" start :icon="link.icon" />
{{ link.title }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
<!-- External Links -->
<v-col cols="5" sm="3">
<v-list density="compact">
<v-list-subheader>Get Involved</v-list-subheader>
<v-list-item
v-for="link in externalLinks"
:key="link.title"
link
slim
:href="link.href"
:to="link.to"
:target="link.href ? '_blank' : undefined"
>
<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" />
{{ link.title }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-col>
<!-- Copyright -->
<v-col
class="text-center serif copyright d-flex align-center justify-center text-grey-darken-1"
cols="12"
sm="6"
>
<div>
<p>&copy; {{ currentYear }} DeFlock. All Rights Reserved</p>
<p>Map data © <a href="https://www.openstreetmap.org/copyright" target="_blank" style="color: unset; font-weight: normal;">OpenStreetMap contributors</a></p>
<p class="mt-4">v1.0</p>
</div>
</v-col>
</v-row>
</v-container>
</v-footer>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useTheme } from 'vuetify';
const theme = useTheme();
const isDark = computed(() => theme.name.value === 'dark');
const currentYear = new Date().getFullYear();
const internalLinks = [
{ title: 'About', to: '/about', icon: 'mdi-information' },
{ title: 'Privacy Policy', to: '/privacy', icon: 'mdi-shield' },
{ title: 'Terms of Service', to: '/terms', icon: 'mdi-file-document' },
{ title: 'Contact', to: '/contact', icon: 'mdi-email' },
];
const externalLinks = [
{ title: 'Discord', href: 'https://discord.gg/aV7v4R3sKT', customIcon: '/icon-discord.svg', customIconDark: '/icon-discord-white.svg' },
{ title: 'Donate', to: '/donate', icon: 'mdi-heart' },
{ title: 'GitHub', href: 'https://github.com/FoggedLens/deflock', icon: 'mdi-github' },
]
</script>
<style scoped>
.custom-icon {
opacity: var(--v-medium-emphasis-opacity);
}
.copyright {
font-size: 0.85rem;
}
</style>

View File

@@ -1,6 +1,7 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { createHead } from '@unhead/vue'
@@ -16,12 +17,21 @@ const vuetify = createVuetify({
directives,
theme: {
defaultTheme: 'light',
themes: {
light: {
colors: {
background: '#f5f5f5',
}
}
}
}
})
const pinia = createPinia()
const app = createApp(App)
app.use(createHead())
app.use(router)
app.use(vuetify)
app.use(pinia)
app.mount('#app')

View File

@@ -83,17 +83,25 @@ const router = createRouter({
}
},
{
path: '/legal',
name: 'legal',
component: () => import('../views/LegalView.vue'),
path: '/terms',
name: 'terms',
component: () => import('../views/TermsOfService.vue'),
meta: {
title: 'Legal | DeFlock'
title: 'Terms of Service | DeFlock'
}
},
{
path: '/privacy',
name: 'privacy',
component: () => import('../views/PrivacyPolicy.vue'),
meta: {
title: 'Privacy Policy | DeFlock'
}
},
{
path: '/qr',
name: 'qr-landing',
component: () => import('../views/QRLandingView.vue'),
component: () => import('../views/Landing.vue'),
meta: {
title: 'You Found an ALPR | DeFlock'
}

View File

@@ -76,12 +76,6 @@ export const getALPRCounts = async () => {
return response.data;
}
export const getClusters = async () => {
const s3Url = "https://deflock-clusters.s3.us-east-1.amazonaws.com/alpr_clusters.json";
const response = await apiService.get(s3Url);
return response.data;
}
export const getCities = async () => {
const s3Url = "https://deflock-clusters.s3.us-east-1.amazonaws.com/flock_cameras_null.json";
const response = await apiService.get(s3Url);

View File

@@ -0,0 +1,32 @@
import { defineStore } from 'pinia';
import { ref, type Ref } from 'vue';
export const useGlobalStore = defineStore('global', () => {
const currentLocation: Ref<[number, number] | null> = ref(null);
const setCurrentLocation = (): Promise<[number, number]> =>
new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
currentLocation.value = [position.coords.latitude, position.coords.longitude];
resolve([position.coords.latitude, position.coords.longitude]);
},
(error) => {
reject(error);
},
{
timeout: 10000,
enableHighAccuracy: true,
}
);
} else {
reject(new Error('Geolocation is not supported by this browser.'));
}
});
return {
currentLocation,
setCurrentLocation,
};
});

101
webapp/src/stores/tiles.ts Normal file
View File

@@ -0,0 +1,101 @@
import { defineStore } from 'pinia';
import { computed, ref, type Ref } from 'vue';
import type { ALPR } from '@/types';
import axios from 'axios';
import type { BoundingBox } from '@/services/apiService'; // TODO: this is a strange place to hold this type
const api = axios.create({
baseURL: 'https://cdn.deflock.me/regions',
});
export const useTilesStore = defineStore('tiles', () => {
// Key: "lat/lng", Value: ALPR[]
const tiles: Ref<Record<string, ALPR[]>> = ref({});
const availableTiles: Ref<string[]> = ref([]);
const expirationDateUtc: Ref<Date | null> = ref(null);
const fetchingTiles = new Set<string>();
let tileUrlTemplate: string|undefined = undefined;
let tileSizeDegrees: number|undefined = undefined;
const fetchIndex = async (): Promise<void> => {
if (expirationDateUtc.value && expirationDateUtc.value > new Date()) {
console.debug('Index is not expired, using cached index');
} else {
console.debug('Index is expired or not set, fetching new index');
const response = await api.get('/index.json');
expirationDateUtc.value = new Date(response.data.expiration_utc);
availableTiles.value = response.data.regions;
tileUrlTemplate = response.data.tile_url;
tileSizeDegrees = response.data.tile_size_degrees;
}
}
const fetchAndAddTile = async (lat: number, lng: number): Promise<void> => {
const key = `${lat}/${lng}`;
if (fetchingTiles.has(key)) {
console.debug(`Tile ${key} is already being fetched, skipping fetch`);
return;
}
if (tiles.value[key]) {
console.debug(`Tile ${key} is already cached, skipping fetch`);
return;
}
if (!tileUrlTemplate) {
console.warn('Tile URL template is not set, skipping fetch');
return;
}
const url = tileUrlTemplate.replace('{lat}/{lon}', key);
try {
fetchingTiles.add(key);
const tile = await api.get(url);
tiles.value[key] = tile.data;
} catch (error) {
console.error(`Failed to fetch tile ${key}:`, error);
} finally {
fetchingTiles.delete(key);
}
}
const fetchVisibleTiles = async (boundingBox: BoundingBox): Promise<void> => {
if (!tileUrlTemplate || !tileSizeDegrees) {
console.debug('Tile URL template or tile size is not set, fetching...');
await fetchIndex();
}
if (!tileUrlTemplate || !tileSizeDegrees) {
console.warn('Tile URL template or tile size is still not set after fetching index');
return;
}
const { minLat: south, minLng: west, maxLat: north, maxLng: east } = boundingBox;
// Determine tiles in viewport
const visibleTiles = [];
for (let lat = Math.floor(south / tileSizeDegrees) * tileSizeDegrees; lat <= Math.ceil(north / tileSizeDegrees) * tileSizeDegrees; lat += tileSizeDegrees) {
for (let lng = Math.floor(west / tileSizeDegrees) * tileSizeDegrees; lng <= Math.ceil(east / tileSizeDegrees) * tileSizeDegrees; lng += tileSizeDegrees) {
const key = `${lat}/${lng}`;
if (!tiles.value[key] && availableTiles.value.includes(key)) {
visibleTiles.push({ lat, lng });
}
}
}
// Fetch missing tiles
const fetchPromises = visibleTiles.map(({ lat, lng }) =>
fetchAndAddTile(lat, lng)
);
await Promise.all(fetchPromises);
}
const allNodes = computed(() => Object.values(tiles.value).flat());
return {
fetchVisibleTiles,
allNodes,
};
});

View File

@@ -1,5 +1,5 @@
<template>
<v-container max-width="1000">
<v-container max-width="1000" class="mb-16">
<h2>About Us</h2>
<p>
Welcome to DeFlock, your go-to resource for understanding and addressing the growing presence of Automated License Plate Readers (ALPRs) in our communities.
@@ -48,22 +48,13 @@
Share our site with your friends, family, and social networks to help raise awareness about the dangers of ALPRs.
</p>
</div>
<v-footer class="my-6">
<v-col cols="7" class="text-grey">
&copy; {{ thisYear }} DeFlock. All rights reserved.
</v-col>
<v-spacer></v-spacer>
<v-col cols="5">
<p><router-link class="text-grey" to="/legal">Legal</router-link></p>
<p><router-link class="text-grey" to="/contact">Contact Us</router-link></p>
</v-col>
</v-footer>
</v-container>
<Footer />
</template>
<script setup lang="ts">
const thisYear = new Date().getFullYear();
import Footer from '@/components/layout/Footer.vue';
</script>
<style scoped>

View File

@@ -1,14 +1,63 @@
<template>
<v-container max-width="1000">
<h2>Contact Us</h2>
<p>
If you have any questions or concerns about the data on this site, please contact us at <a href="mailto:contact@deflock.me">contact@deflock.me</a>.
</p>
<v-container class="mb-16 text-center">
<h1>Contact Us</h1>
<div>
<p>
Have questions about DeFlock? Need help contributing to OpenStreetMap?
</p>
<p>
Reach out using the contact options below.
</p>
</div>
<v-row class="mt-12">
<v-col cols="12" sm="6">
<v-card
variant="outlined"
append-icon="mdi-open-in-new"
class="mx-auto my-2"
href="mailto:contact@deflock.me"
max-width="344"
prepend-icon="mdi-email"
subtitle="contact@deflock.me"
target="_blank"
title="Email Us"
></v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card
variant="outlined"
append-icon="mdi-open-in-new"
class="mx-auto my-2"
href="https://discord.gg/aV7v4R3sKT"
max-width="344"
subtitle="discord.gg/aV7v4R3sKT"
target="_blank"
title="Join our Discord"
>
<template v-slot:prepend>
<v-img class="mr-2" contain width="24" height="24" :src="isDark ? 'icon-discord-white.svg' : 'icon-discord.svg'" />
</template>
</v-card>
</v-col>
</v-row>
</v-container>
<Footer />
</template>
<style scoped>
p {
margin-top: 0.5rem;
}
<script setup lang="ts">
import Footer from '@/components/layout/Footer.vue';
import { useTheme } from 'vuetify';
import { computed } from 'vue';
const theme = useTheme();
const isDark = computed(() => theme.name.value === 'dark');
</script>
<style lang="css" scoped>
@import url('@/assets/typography.css');
</style>

View File

@@ -1,5 +1,5 @@
<template>
<v-container class="info-section">
<v-container class="info-section mb-16">
<!-- Hero Section -->
<v-row justify="center" class="hero-section-harms text-center mb-4">
<v-col cols="12" md="8">
@@ -21,12 +21,15 @@
</v-col>
</v-row>
<dangers />
<dangers showAll />
</v-container>
<Footer />
</template>
<script setup lang="ts">
import Dangers from '@/components/Dangers.vue';
import Footer from '@/components/layout/Footer.vue';
</script>
<style scoped>
@@ -57,8 +60,4 @@ import Dangers from '@/components/Dangers.vue';
p {
margin-bottom: 16px;
}
.info-section {
background: var(--df-page-background-color);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<v-container class="sponsor-page">
<v-container fluid class="sponsor-page">
<!-- Hero Section -->
<v-row justify="center" class="hero-section-sponsor text-center mb-4">
<v-col cols="12" md="8">
@@ -14,10 +14,17 @@
<!-- GitHub Sponsors Section -->
<v-row justify="center" class="sponsors-section text-center">
<v-col cols="12" md="10">
<h2 class="mb-4">Our Amazing Sponsors</h2>
<h2 class="mb-2">Our Amazing Sponsors</h2>
<p class="mb-8">
Want to see your name here? <a target="_blank" href="https://github.com/sponsors/frillweeman">Become a sponsor</a>, and your name will appear on this page!
</p>
<v-row>
<v-col v-for="sponsor in sponsors" :key="sponsor.login" cols="6" md="4" lg="3">
<v-card variant="flat" class="text-center py-2">
<v-col v-if="isLoadingSponsors" v-for="n in 4" cols="6" md="4" lg="3">
<v-skeleton-loader type="image"></v-skeleton-loader>
</v-col>
<v-col v-else v-for="sponsor in sponsors" :key="sponsor.login" cols="6" md="4" lg="3">
<v-card :href="sponsor.url" target="_blank" variant="flat" class="text-center py-2" color="transparent">
<v-avatar size="64px" class="mb-3">
<v-img :src="sponsor.avatarUrl" :alt="sponsor.name" />
</v-avatar>
@@ -27,29 +34,40 @@
</v-row>
</v-col>
</v-row>
</v-container>
<!-- Footer Section -->
<v-footer class="text-center mt-8">
<v-row>
<v-col>
<p>&copy; {{ new Date().getFullYear() }} DeFlock. All rights reserved.</p>
<!-- GitHub Sponsors Section -->
<v-row justify="center" class="sponsors-section text-center mt-4">
<v-col cols="12" md="10">
<h2 class="mb-2">Special Thanks</h2>
<v-card href="https://www.404media.co/" target="_blank" max-width="250" variant="flat" class="text-center py-2 mx-auto" color="transparent">
<v-img class="ma-4" src="/404media.svg" alt="404 Media Logo" contain />
<v-card-title class="mt-2 serif text-center font-weight-bold">404 Media</v-card-title>
<v-card-text>
<p>Special thanks to Jason Koebler at 404 Media for popularizing this project. Be sure to support them, as they are fellow privacy advocates.</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-footer>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import { ref, onMounted, type Ref } from "vue";
import { getSponsors } from "@/services/apiService";
import Footer from "@/components/layout/Footer.vue";
interface Sponsor {
login: string;
name: string;
avatarUrl: string;
url: string;
}
const sponsors: Ref<Sponsor[]> = ref([]);
const isLoadingSponsors = ref(true);
onMounted(() => {
getSponsors()
@@ -58,6 +76,9 @@ onMounted(() => {
})
.catch((error) => {
console.error(error);
})
.finally(() => {
isLoadingSponsors.value = false;
});
});
</script>

View File

@@ -1,78 +1,97 @@
<template>
<!-- Hero Section -->
<v-container fluid class="hero-section">
<v-row justify="center">
<v-col cols="12" md="8" class="text-center">
<h1 class="display-1 px-8">You're Being Tracked by ALPRs!</h1>
<ALPRCounter class="mt-4" />
<p class="subtitle-1 px-8 mt-6 mb-12 bigger">
Automated License Plate Readers (ALPRs) are monitoring your every move. Learn more about how they work and how you can protect your privacy.
</p>
<v-btn color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
Find Nearby ALPRs
<v-icon end>mdi-map</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
<v-row justify="center">
<v-col cols="12" md="8" class="text-center">
<h1 class="display-1 px-8">You're Being Tracked</h1>
<ALPRCounter class="my-6" />
<!-- Featured On Section -->
<v-container class="featured-on-section my-8">
<h4 class="mb-4" style="opacity: 0.8">Featured On</h4>
<v-row justify="center" align-items="center">
<v-card flat v-for="site in featuredOn" class="mx-4" :width="site.wide ? 200 : 100" height="50" style="background: rgba(0,0,0,0)">
<v-img contain :src="site.logo" :alt="site.name" class="featured-logo" style="display: flex; align-items: center; height: 100%;" />
</v-card>
</v-row>
</v-container>
<!-- Dangers Section -->
<v-container class="py-10 text-center info-section">
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
<p class="subtitle-1 px-8">
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements, profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
</p>
<v-row class="align-center mt-4">
<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>
Privacy Violations
</v-card-title>
<v-card-text>
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history. They surveil mostly innocent people while claiming to target criminals.
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4" class="text-center">
<v-card>
<v-card-title class="headline">
<v-icon x-large class="mr-2">mdi-alert-circle</v-icon>
Risk of Misuse
</v-card-title>
<v-card-text>
Data from ALPRs has led to <a target="_blank" href="https://www.newsobserver.com/news/state/north-carolina/article287381160.html">wrongful arrests</a>, profiling, and <a target="_blank" href="https://www.kwch.com/2022/10/31/kechi-police-lieutenant-arrested-using-police-technology-stalk-wife/">stalking ex-partners</a> by police officers.
</v-card-text>
</v-card>
</v-col>
<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>
Limited Benefits
</v-card-title>
<v-card-text>
There's no substantial evidence that ALPRs effectively prevent crime, despite <a target="_blank" href="https://www.404media.co/researcher-who-oversaw-flock-surveillance-study-now-has-concerns-about-it/">Flock's unethical attempts</a> to prove otherwise.
</v-card-text>
</v-card>
</v-col>
</v-row>
<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-btn>
</v-col>
</v-row>
</v-container>
<!-- Dangers Section -->
<v-container class="py-10 text-center info-section">
<v-btn class="mt-8" color="rgb(18, 151, 195)" large to="/dangers">
See All Dangers
<v-icon end>mdi-shield-alert</v-icon>
</v-btn>
</v-container>
<h2 class="mb-4">What is an ALPR</h2>
<p class="text-left px-6">
Automated License Plate Readers (ALPRs) are cameras that capture images of all passing license plates, storing details like the car's location, date, and time. 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.
</p>
<!-- Map Section -->
<v-container class="map-section py-10 text-center">
<h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2>
<v-btn color="white" large @click="goToMap">
View the Map
<v-icon end>mdi-map</v-icon>
</v-btn>
</v-container>
<v-divider class="my-8" />
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
<p class="px-6">
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements and profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
</p>
<v-row class="align-center mt-4">
<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>
Privacy Violations
</v-card-title>
<v-card-text>
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history.
</v-card-text>
</v-card>
</v-col>
<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>
Risk of Misuse
</v-card-title>
<v-card-text>
Data from ALPRs has led to <a target="_blank" href="https://www.newsobserver.com/news/state/north-carolina/article287381160.html">wrongful arrests</a>, profiling, and <a target="_blank" href="https://www.kwch.com/2022/10/31/kechi-police-lieutenant-arrested-using-police-technology-stalk-wife/">stalking ex-partners</a> by police officers.
</v-card-text>
</v-card>
</v-col>
<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>
Limited Benefits
</v-card-title>
<v-card-text>
There's no substantial evidence that ALPRs effectively prevent crime, despite <a target="_blank" href="https://www.404media.co/researcher-who-oversaw-flock-surveillance-study-now-has-concerns-about-it/">Flock's unethical attempts</a> to prove otherwise.
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-btn class="mt-8" color="rgb(18, 151, 195)" large to="/what-is-an-alpr">
<v-icon start>mdi-book-open-page-variant</v-icon>
Read More
</v-btn>
</v-container>
<!-- Map Section -->
<v-container fluid class="map-section py-10 text-center">
<h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2>
<v-btn color="white" large @click="goToMap">
View the Map
<v-icon end>mdi-map</v-icon>
</v-btn>
</v-container>
<Footer />
</template>
<style>
@@ -80,112 +99,103 @@
background: url('/flock-camera.jpeg') no-repeat center center;
background-size: cover;
color: white;
padding: 100px 0 !important;
padding: 60px 0 50px 0 !important;
position: relative;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.hero-section > * {
position: relative;
z-index: 2;
}
.info-section {
background: var(--df-page-background-color);
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.55);
z-index: 1;
}
.hero-section > * {
position: relative;
z-index: 2;
}
.map-section {
background: url('/deflock-screenshot.webp') no-repeat center center;
background-size: cover;
color: white;
padding: 100px 0;
position: relative;
}
.map-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
background: url('/deflock-screenshot.webp') no-repeat center center;
background-size: cover;
color: white;
padding: 100px 0;
position: relative;
}
.map-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.map-section > * {
position: relative;
z-index: 2;
.map-section > * {
position: relative;
z-index: 2;
}
.bigger {
font-size: 1.1rem;
}
.featured-logo {
opacity: 0.7;
}
</style>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import ALPRCounter from '@/components/ALPRCounter.vue';
import { useGlobalStore } from '@/stores/global';
import Footer from '@/components/layout/Footer.vue';
const router = useRouter();
const userLocation = ref<[number, number] | null>(null);
async function fetchUserLocation(): Promise<[number, number]> {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve([position.coords.latitude, position.coords.longitude]);
},
(error) => {
reject(error);
},
{
timeout: 10000,
enableHighAccuracy: true,
}
);
} else {
reject(new Error('Geolocation is not supported by this browser.'));
}
});
}
async function getUserLocation() {
try {
const [lat, lon] = await fetchUserLocation();
userLocation.value = [lat, lon];
} catch (error) {
console.debug('Error fetching user location:', error);
}
}
const { setCurrentLocation } = useGlobalStore();
interface GoToMapOptions {
withCurrentLocation?: boolean;
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) {
await getUserLocation();
if (userLocation.value) {
const [lat, lon] = userLocation.value;
router.push({ path: '/map', hash: `#map=14/${lat.toFixed(6)}/${lon.toFixed(6)}` });
} else {
router.push({ path: '/map', hash: '#map=14/40.0150/-105.2705' });
}
} else {
router.push({ path: '/map' });
}
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>

View File

@@ -1,84 +0,0 @@
<template>
<v-container max-width="1000">
<h1>Legal</h1>
<h2>Attribution</h2>
<p>
This website uses data from <a href="https://openstreetmap.org" target="_blank">OpenStreetMap</a>. OpenStreetMap is open data, licensed under the <a href="https://opendatacommons.org/licenses/odbl/" target="_blank">Open Data Commons Open Database License (ODbL)</a>.
</p>
<p>
Geocoding services are provided by <a href="https://nominatim.org/" target="_blank">Nominatim</a>, a project of OpenStreetMap.
</p>
<h2>Privacy Policy</h2>
<h3>Plausible Analytics</h3>
<p>
We use Plausible Analytics to track anonymous usage data. Plausible does not use cookies or collect personally identifiable information. The data collected is used solely to understand how people are using the site and to improve user experience.
</p>
<p>
No data is shared with third parties, and no personally identifiable information is stored or collected.
</p>
<h3>User Reports</h3>
<p>
At this time, DeFlock.me does not collect or store user-submitted reports. Users are directed to submit ALPR camera locations directly to OpenStreetMap via their editing platform, which has its own privacy policy and data practices.
</p>
<h2>Terms of Use</h2>
<h3>Use of the Website</h3>
<p>
DeFlock.me provides a service to help users locate and report Automated License Plate Reader (ALPR) cameras through external services like OpenStreetMap. By using this site, you agree to use the tools and resources in a lawful and ethical manner.
</p>
<h3>Liability Disclaimer</h3>
<p>
DeFlock.me provides information and directs users to third-party services for ALPR reporting. We do not guarantee the accuracy or completeness of the data presented on the map.
</p>
<p>
DeFlock.me and its operators are not liable for any direct or indirect damages arising from the use of the site, including reliance on third-party data or the submission of ALPR locations via OpenStreetMap.
</p>
<h2>User-Generated Content</h2>
<p>
DeFlock.me does not currently accept user-generated reports directly. All ALPR camera reports are directed to OpenStreetMap&apos;s editing platform. Users should follow OSM&apos;s guidelines and policies when submitting data.
</p>
<p>
In the future, we may add the ability to report ALPR cameras directly on this site. At that time, additional policies will be introduced regarding the use and storage of user-generated content.
</p>
<h2>Copyright Notice</h2>
<p>
All original content, design, and features of DeFlock.me are © {{ thisYear }} DeFlock.me. All rights reserved. Reproduction, distribution, or use of site content without permission is prohibited.
</p>
<p>
OpenStreetMap data is © OpenStreetMap contributors and is licensed under the Open Data Commons Open Database License (ODbL).
</p>
</v-container>
</template>
<script setup lang="ts">
const thisYear = new Date().getFullYear();
</script>
<style scoped>
/* TODO: put this all in one place, also in what-is view */
h2 {
margin-top: 2rem;
}
h3 {
margin-top: 1.25rem;
}
p {
margin-top: 0.5rem;
}
/* as you can see, this one is the most updated */
a {
color: var(green);
text-decoration: none;
}
</style>

View File

@@ -1,23 +1,15 @@
<template>
<div class="map-container" @keyup="handleKeyUp">
<!-- <NewVisitor /> -->
<v-card class="map-notif" v-show="isLoadingALPRs && !showClusters">
<v-card-title><v-progress-circular indeterminate color="primary" /></v-card-title>
</v-card>
<!-- use-global-leaflet=false is a workaround for a bug in current version of vue-leaflet -->
<l-map
<leaflet-map
v-if="center"
ref="map"
v-model:zoom="zoom"
v-model:center="center"
:use-global-leaflet="false"
v-model:zoom="zoom"
:current-location="currentLocation"
@update:bounds="updateBounds"
@ready="mapLoaded"
:options="{ zoomControl: false }"
:alprs
>
<l-control position="topleft">
<!-- SEARCH -->
<template v-slot:topleft>
<form @submit.prevent="onSearch">
<v-text-field
:rounded="xs || undefined"
@@ -40,26 +32,14 @@
</template>
</v-text-field>
</form>
</l-control>
<!-- url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" -->
</template>
<l-tile-layer
:url="mapTileUrl"
layer-type="base"
name="OpenStreetMap"
attribution="&copy; <a target=&quot;_blank&quot; href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
/>
<l-control position="bottomright">
<v-btn @click="goToUserLocation" icon class="mt-2">
<v-icon x-large>mdi-crosshairs-gps</v-icon>
</v-btn>
</l-control>
<DFMarkerCluster v-if="showClusters" v-for="cluster in clusters" :key="cluster.id" :lat="cluster.lat" :lon="cluster.lon" />
<DFMapMarker v-else v-for="alpr in visibleALPRs" :key="alpr.id" :alpr :show-fov="zoom >= 16" />
</l-map>
<div class="loader" v-else>
<!-- CURRENT LOCATION -->
<template v-slot:bottomright>
<v-fab icon="mdi-crosshairs-gps" @click="goToUserLocation" />
</template>
</leaflet-map>
<div v-else class="loader">
<span class="mb-4 text-grey">Loading Map</span>
<v-progress-circular indeterminate color="primary" />
</div>
@@ -68,22 +48,21 @@
<script setup lang="ts">
import 'leaflet/dist/leaflet.css';
import { LMap, LTileLayer, LControl } from '@vue-leaflet/vue-leaflet';
import { ref, onMounted, computed, watch } from 'vue';
import { useRouter } from 'vue-router'
import type { Ref } from 'vue';
import { BoundingBox } from '@/services/apiService';
import type { Cluster } from '@/services/apiService';
import { getALPRs, geocodeQuery, getClusters } from '@/services/apiService';
import { geocodeQuery } from '@/services/apiService';
import { useDisplay, useTheme } from 'vuetify';
import DFMapMarker from '@/components/DFMapMarker.vue';
import DFMarkerCluster from '@/components/DFMarkerCluster.vue';
import NewVisitor from '@/components/NewVisitor.vue';
import { useGlobalStore } from '@/stores/global';
import { useTilesStore } from '@/stores/tiles';
import type { ALPR } from '@/types';
import L from 'leaflet';
globalThis.L = L;
import 'leaflet/dist/leaflet.css'
import LeafletMap from '@/components/LeafletMap.vue';
const DEFAULT_ZOOM = 12;
const MIN_ZOOM_FOR_REFRESH = 4;
const CLUSTER_ZOOM_THRESHOLD = 9;
const theme = useTheme();
const zoom: Ref<number> = ref(DEFAULT_ZOOM);
@@ -91,25 +70,18 @@ const center: Ref<any|null> = ref(null);
const bounds: Ref<BoundingBox|null> = ref(null);
const searchField: Ref<any|null> = ref(null);
const searchQuery: Ref<string> = ref('');
const tilesStore = useTilesStore();
const { fetchVisibleTiles } = tilesStore;
const alprs = computed(() => tilesStore.allNodes);
const router = useRouter();
const { xs } = useDisplay();
const canRefreshMarkers = computed(() => zoom.value >= MIN_ZOOM_FOR_REFRESH);
const mapTileUrl = computed(() =>
theme.global.name.value === 'dark' ?
'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' :
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
);
const globalStore = useGlobalStore();
const alprs: Ref<ALPR[]> = ref([]);
const clusters: Ref<Cluster[]> = ref([]);
const bboxForLastRequest: Ref<BoundingBox|null> = ref(null);
const showClusters = computed(() => zoom.value <= CLUSTER_ZOOM_THRESHOLD);
const isLoadingALPRs = ref(false);
const visibleALPRs = computed(() => {
return alprs.value.filter(alpr => bounds.value?.containsPoint(alpr.lat, alpr.lon));
});
const setCurrentLocation = globalStore.setCurrentLocation;
const currentLocation = computed(() => globalStore.currentLocation);
function handleKeyUp(event: KeyboardEvent) {
if (event.key === '/' && searchField.value.value !== document.activeElement) {
@@ -137,40 +109,18 @@ function onSearch() {
}
function goToUserLocation() {
getUserLocation()
.then(location => {
center.value = { lat: location[0], lng: location[1] };
zoom.value = DEFAULT_ZOOM;
}).catch(error => {
setCurrentLocation()
.then((cl) => {
center.value = cl;
setTimeout(() => {
zoom.value = DEFAULT_ZOOM;
}, 10);
})
.catch(error => {
console.debug('Error getting user location.', error);
});
}
function getUserLocation(): Promise<[number, number]> {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve([position.coords.latitude, position.coords.longitude]);
},
(error) => {
reject(error);
},
{
timeout: 10000,
enableHighAccuracy: true,
}
);
} else {
reject(new Error('Geolocation is not supported by this browser.'));
}
});
};
function mapLoaded(map: any) {
updateBounds(map.getBounds());
}
function updateBounds(newBounds: any) {
updateURL();
@@ -182,11 +132,6 @@ function updateBounds(newBounds: any) {
});
bounds.value = newBoundingBox;
if (bboxForLastRequest.value && newBoundingBox.isSubsetOf(bboxForLastRequest.value)) {
console.debug('new bounds are a subset of the last request, skipping');
return;
}
updateMarkers();
}
@@ -200,46 +145,16 @@ function updateURL() {
});
}
watch(zoom, (newZoom, oldZoom) => {
if (newZoom <= CLUSTER_ZOOM_THRESHOLD && oldZoom > CLUSTER_ZOOM_THRESHOLD) {
bboxForLastRequest.value = bounds.value;
} else if (newZoom < CLUSTER_ZOOM_THRESHOLD) {
alprs.value = [];
bboxForLastRequest.value = null;
}
});
function updateMarkers() {
// Fetch ALPRs in the current view
if (!bounds.value) {
return;
}
if (showClusters.value || !canRefreshMarkers.value) {
return;
}
isLoadingALPRs.value = true;
getALPRs(bounds.value)
.then((result: any) => {
// merge incoming with existing, so that moving the map doesn't remove markers
const existingIds = new Set(alprs.value.map(alpr => alpr.id));
const newAlprs = result.elements.filter((alpr: any) => !existingIds.has(alpr.id));
alprs.value = [...alprs.value, ...newAlprs];
bboxForLastRequest.value = bounds.value;
})
.finally(() => {
isLoadingALPRs.value = false;
});
fetchVisibleTiles(bounds.value);
}
onMounted(() => {
getClusters()
.then((result: any) => {
clusters.value = result.clusters;
});
const hash = router.currentRoute.value.hash;
if (hash) {
const parts = hash.split('/');
@@ -253,7 +168,7 @@ onMounted(() => {
}
} else {
// show US map by default
zoom.value = 4;
zoom.value = 5;
center.value = { lat: 39.8283, lng: -98.5795 };
}
});
@@ -270,13 +185,14 @@ onMounted(() => {
.map-container {
width: 100%;
height: calc(100dvh - 64px);
overflow: auto;
}
.map-search {
width: calc(100vw - 22px);
max-width: 400px;
@media (min-width: 600px) {
max-width: 320px;
}
z-index: 1000;
}

View File

@@ -51,12 +51,15 @@
We are getting a lot of new datasets and trying to decide how to import them at a large scale, since they need to be verified and deduplicated. If you have any ideas or want to help, please reach out to us <router-link to="/contact">here</router-link> or join our <a href="https://discord.gg/aV7v4R3sKT" target="_blank">Discord</a>.
</p>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import { onMounted, ref, computed, watch } from 'vue';
import { getCities } from '@/services/apiService';
import CommunityDatasets from '@/components/CommunityDatasets.vue';
import Footer from '@/components/layout/Footer.vue';
const page = ref(1);
const selectedState = ref('');
@@ -125,8 +128,4 @@ code {
border-radius: 0.25rem;
font-weight: bold;
}
.info-section {
background: var(--df-page-background-color);
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<v-container class="mb-16">
<h1>Privacy Policy</h1>
<p>Effective Date: <b>12/26/2024</b></p>
<p>
At DeFlock, we respect your privacy. This privacy policy outlines our approach to user privacy and how information may be handled when using our platform.
</p>
<h2>Information We Collect</h2>
<p>
DeFlock does not collect, store, or process any personal information about our users. We do not use cookies, analytics, or tracking technologies on our website.
</p>
<h2>Third-Party Services</h2>
<p>
DeFlock relies on OpenStreetMap (OSM) for map data and functionality. If you choose to contribute Automatic License Plate Recognition (ALPR) data or other content to OSM, you will interact directly with their platform. OSM may request personal information, such as your email address and name, to facilitate your contributions. Please refer to the OpenStreetMap Privacy Policy for details on their data practices.
</p>
<h2>Your Choices</h2>
<p>
If you prefer not to share personal information, you are not required to contribute to OSM to use DeFlock. You can still enjoy the map data we source from OSM without uploading additional information.
</p>
<h2>Contact Us</h2>
<p>
If you have any questions or concerns about this privacy policy, please contact us at:
</p>
<p>
Email: <a href="mailto:contact@deflock.me">contact@deflock.me</a>
</p>
<h2>Policy Updates</h2>
<p>
We may update this privacy policy from time to time to reflect changes in our practices or for legal compliance. The "Effective Date" above will indicate when the latest changes were made.
</p>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import Footer from '@/components/layout/Footer.vue';
</script>
<style lang="css" scoped>
@import url('@/assets/typography.css');
</style>

View File

@@ -1,191 +0,0 @@
<template>
<!-- Hero Section -->
<v-container fluid class="hero-section">
<v-row justify="center">
<v-col cols="12" md="8" class="text-center">
<h1 class="display-1 px-8">You're Being Tracked by an ALPR!</h1>
<ALPRCounter class="mt-4" />
<p class="subtitle-1 px-8 mt-6 mb-12 bigger">
Automated License Plate Readers (ALPRs) are monitoring your every move. Learn more about how they work and how you can protect your privacy.
</p>
<v-btn color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
Find the Nearest ALPR
<v-icon end>mdi-map</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
<!-- Dangers Section -->
<v-container class="py-10 text-center info-section">
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
<p class="subtitle-1 px-8">
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements, profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
</p>
<v-row class="align-center mt-4">
<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>
Privacy Violations
</v-card-title>
<v-card-text>
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history. They surveil mostly innocent people while claiming to target criminals.
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="4" class="text-center">
<v-card>
<v-card-title class="headline">
<v-icon x-large class="mr-2">mdi-alert-circle</v-icon>
Risk of Misuse
</v-card-title>
<v-card-text>
Data from ALPRs has led to <a target="_blank" href="https://www.newsobserver.com/news/state/north-carolina/article287381160.html">wrongful arrests</a>, profiling, and <a target="_blank" href="https://www.kwch.com/2022/10/31/kechi-police-lieutenant-arrested-using-police-technology-stalk-wife/">stalking ex-partners</a> by police officers.
</v-card-text>
</v-card>
</v-col>
<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>
Limited Benefits
</v-card-title>
<v-card-text>
There's no substantial evidence that ALPRs effectively prevent crime, despite <a target="_blank" href="https://www.404media.co/researcher-who-oversaw-flock-surveillance-study-now-has-concerns-about-it/">Flock's unethical attempts</a> to prove otherwise.
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-btn class="mt-8" color="rgb(18, 151, 195)" large to="/dangers">
See All Dangers
<v-icon end>mdi-shield-alert</v-icon>
</v-btn>
</v-container>
<!-- Map Section -->
<v-container class="map-section py-10 text-center">
<h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2>
<v-btn color="white" large @click="goToMap">
View the Map
<v-icon end>mdi-map</v-icon>
</v-btn>
</v-container>
</template>
<style>
.hero-section {
background: url('/flock-camera.jpeg') no-repeat center center;
background-size: cover;
color: white;
padding: 100px 0 !important;
position: relative;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.hero-section > * {
position: relative;
z-index: 2;
}
.info-section {
background: #f5f5f5;
}
.map-section {
background: url('/deflock-screenshot.webp') no-repeat center center;
background-size: cover;
color: white;
padding: 100px 0;
position: relative;
}
.map-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.map-section > * {
position: relative;
z-index: 2;
}
.bigger {
font-size: 1.1rem;
}
</style>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import ALPRCounter from '@/components/ALPRCounter.vue';
const router = useRouter();
const userLocation = ref<[number, number] | null>(null);
async function fetchUserLocation(): Promise<[number, number]> {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve([position.coords.latitude, position.coords.longitude]);
},
(error) => {
reject(error);
},
{
timeout: 10000,
enableHighAccuracy: true,
}
);
} else {
reject(new Error('Geolocation is not supported by this browser.'));
}
});
}
async function getUserLocation() {
try {
const [lat, lon] = await fetchUserLocation();
userLocation.value = [lat, lon];
} catch (error) {
console.debug('Error fetching user location:', error);
}
}
interface GoToMapOptions {
withCurrentLocation?: boolean;
}
async function goToMap(options: GoToMapOptions = {}) {
if (options.withCurrentLocation) {
await getUserLocation();
if (userLocation.value) {
const [lat, lon] = userLocation.value;
router.push({ path: '/map', hash: `#map=16/${lat.toFixed(6)}/${lon.toFixed(6)}` });
} else {
router.push({ path: '/map', hash: '#map=16/40.0150/-105.2705' });
}
} else {
router.push({ path: '/map' });
}
}
</script>

View File

@@ -1,125 +1,148 @@
<template>
<v-container class="info-section" max-width="1000">
<v-alert
variant="tonal"
type="info"
class="my-6"
title="Reporting Feature Coming Soon"
>
<p>
We're working on a feature that will allow you to report ALPRs directly on this site. In the meantime, you can follow the steps below to add the ALPR to OpenStreetMap.
</p>
</v-alert>
<h2>How to Report an ALPR</h2>
<v-container>
<h1 class="text-center mt-4">Report a New ALPR</h1>
<p>
If you've spotted an ALPR in your area, you can help us track it by reporting it to OpenStreetMap, where we source our information. Here's how you can do it:
</p>
<v-alert
variant="tonal"
type="warning"
class="my-6"
title="Are you sure it's an ALPR?"
>
<p>
Before you report an ALPR, please read our <router-link style="color: unset !important" to="/what-is-an-alpr#not-alpr">guide on what ALPRs look like</router-link> to make sure you're reporting the right thing.
</p>
</v-alert>
<v-stepper-vertical color="rgb(18, 151, 195)" v-model="step" flat non-linear class="my-8" edit-icon="mdi-home">
<template v-slot:default="{ step }: { step: any }">
<v-stepper-vertical-item
class="transparent"
:complete="step > 1"
title="Create an OpenStreetMap Account"
value="1"
editable
>
<p>
<a href="https://www.openstreetmap.org/user/new" target="_blank">Sign up for an OpenStreetMap account</a> in order to submit changes.
</p>
</v-stepper-vertical-item>
<div class="ml-4 mt-4">
<h3>1. Create an OpenStreetMap Account</h3>
<p>
<a href="https://www.openstreetmap.org/user/new" target="_blank">Sign up for an OpenStreetMap account</a> in order to submit changes.
</p>
<v-stepper-vertical-item
class="transparent"
:complete="step > 2"
title="Find the ALPR's Location"
value="2"
editable
>
<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>
</v-stepper-vertical-item>
<h3>2. Find the ALPR's Location on OpenStreetMap</h3>
<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>
<v-stepper-vertical-item
class="transparent"
:complete="step > 3"
title="Add the ALPR to OpenStreetMap"
value="3"
editable
>
<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>
<v-img max-width="450" src="/edit-map.png" class="my-8" />
<p class="mt-16 mb-8">
To add the ALPR, click the <strong>Point</strong> button in the top left corner of the editor, then click on the location of the ALPR on the map. In the popup that appears, paste one of the following sets of tags based on the brand of the ALPR:
</p>
<h3>3. Add the ALPR to OpenStreetMap</h3>
<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>
<v-divider class="my-4"><span class="serif text-grey-darken-2">Choose a Manufacturer</span></v-divider>
<v-img max-width="450" src="/edit-map.png" class="my-8" />
<OSMTagSelector />
<p>
To add the ALPR, click the <strong>Point</strong> button in the top left corner of the editor, then click on the location of the ALPR on the map. In the popup that appears, paste one of the following sets of tags based on the brand of the ALPR:
</p>
<v-divider class="mb-4 mt-8" />
<p class="mt-8">
After copying the tags, paste them into the <strong>Tags</strong> field in the popup.
</p>
<v-img max-width="450" class="my-8" src="/paste-tags.png" />
</v-stepper-vertical-item>
<OSMTagSelector />
<v-stepper-vertical-item
class="transparent"
:complete="step > 4"
title="Adjust the Direction"
value="4"
editable
>
<v-img max-width="450" class="my-8" src="/adjust-angle.png" />
<p>
If you know the direction that the ALPR is facing, you can use the up and down arrows to set the direction it faces.
</p>
</v-stepper-vertical-item>
<p class="mt-8">
After copying the tags, paste them into the <strong>Tags</strong> field in the popup.
</p>
<v-stepper-vertical-item
class="transparent"
:complete="step > 5"
title="Submit Your Changes"
value="5"
editable
>
<p>
Once you've added the ALPR to the map, click the <strong>Save</strong> button in the top left 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"
class="my-6"
title="How Long Will It Take?"
>
<p>
We pull data from OpenStreetMap <i>daily</i>, so it may take up to 24 hours 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>
</v-alert>
</v-stepper-vertical-item>
<v-img max-width="450" class="my-8" src="/paste-tags.png" />
<v-stepper-vertical-item
class="transparent"
:complete="step > 6"
title="Hang a Sign"
value="6"
editable
>
<p>
Download our <a href="/deflock-poster.pdf" target="_blank">ALPR sign</a> and hang it near the ALPR to help raise awareness about the device. Be sure to follow all local laws and regulations when hanging signs.
</p>
<h3>4. Adjust the Direction</h3>
<template v-slot:next>
<v-btn
color="primary"
disabled
>
Next
</v-btn>
</template>
</v-stepper-vertical-item>
</template>
</v-stepper-vertical>
<v-img max-width="450" class="my-8" src="/adjust-angle.png" />
<h2 class="text-center mt-16">Edit an Existing ALPR</h2>
<p>
If you find an ALPR that's missing information and would like to update it, you can follow the same steps as above. Each ALPR on DeFlock has a Node ID that you can use to find it on OpenStreetMap.
</p>
<p>
If you know the direction that the ALPR is facing, you can use the up and down arrows to set the direction it faces.
</p>
<p class="mb-16">
Simply click on the ALPR with missing information, and find the Node ID (e.g. <code>node/1237489334</code>) at the bottom of the popup. In the OSM editor search field, paste the <i>numerical portion</i> of the Node ID to find the ALPR and make your changes.
</p>
<h3>5. Submit Your Changes</h3>
<p>
Once you've added the ALPR to the map, click the <strong>Save</strong> button in the top left 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>
<h3>6. Hang a Sign</h3>
<p>
Download our <a href="/deflock-poster.pdf" target="_blank">ALPR sign</a> and hang it near the ALPR to help raise awareness about the device. Be sure to follow all local laws and regulations when hanging signs.
</p>
</div>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import DFCode from '@/components/DFCode.vue';
import OSMTagSelector from '@/components/OSMTagSelector.vue';
import Footer from '@/components/layout/Footer.vue';
import { ref } from 'vue';
import { VStepperVertical, VStepperVerticalItem } from 'vuetify/labs/VStepperVertical';
const step = ref(1);
</script>
<style scoped>
/* TODO: put this all in one place, also in what-is view */
h2 {
margin-top: 2rem;
}
@import url('@/assets/typography.css');
h3 {
margin-top: 1.25rem;
}
p {
margin-top: 0.5rem;
}
.flex-image {
display: flex;
gap: 1rem;
align-items: center;
margin-top: 0.5rem;
}
code {
background-color: #f4f4f4;
padding: 0.5rem;
border-radius: 0.25rem;
display: block;
margin-top: 0.5rem;
}
.highlight {
background-color: #0081ac;
padding: 0.15rem;
border-radius: 0.25rem;
font-weight: bold;
}
.info-section {
background: var(--df-page-background-color);
.transparent {
background-color: transparent !important;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<v-container class="mb-16">
<h1>Terms of Service for DeFlock</h1>
<p>Effective Date: <b>12/26/2024</b></p>
<p>
Welcome to DeFlock. By accessing or using our site, you agree to these Terms of Service. If you do not agree, please refrain from using DeFlock.
</p>
<h2>
1. About DeFlock
</h2>
<p>
DeFlock provides access to map data sourced from OpenStreetMap (OSM). Our platform allows users to view maps and provides examples of OSM tags for contributing Automatic License Plate Recognition (ALPR) data or other information to OSM.
</p>
<h2>
2. User Responsibilities
</h2>
<h3>
2.1 Tagging Guidance
</h3>
<p>
While DeFlock provides examples of OSM tags that may be relevant for your contributions, it is the users responsibility to ensure the accuracy and appropriateness of the tags used when uploading to OSM. DeFlock is not liable for any consequences arising from incorrect or inappropriate tagging.
</p>
<h3>
2.2 Prohibited Uses
</h3>
<p>
DeFlock must not be used for any of the following purposes:
</p>
<ul class="serif">
<li>Locating specific nodes, ways, or relations on OpenStreetMap for the purpose of vandalism, including deleting accurate information or modifying it to be false or misleading.</li>
<li>Scraping, harvesting, or systematically collecting data from DeFlock or OpenStreetMap in violation of their terms or for unauthorized purposes.</li>
<li>Impersonating other contributors or providing false information when contributing to OpenStreetMap.</li>
<li>Using DeFlock or its guidance to create commercial products or services without proper attribution or compliance with OpenStreetMap's licensing terms.</li>
<li>Attempting to disrupt or interfere with the functionality of DeFlock, including through hacking, reverse engineering, or introducing malicious software.</li>
<li>Using DeFlock in ways that promote hate speech, harassment, or illegal activities.</li>
</ul>
<p>
By using DeFlock, you agree to comply with all applicable laws and regulations.
</p>
<h2>
3. Third-Party Services
</h2>
<p>
DeFlock integrates with OpenStreetMap, and any interactions with OSM are governed by their terms of use. DeFlock is not responsible for the collection, storage, or use of data by OSM or any third-party services. For more information, please review the OpenStreetMap Terms of Use.
</p>
<h2>4. Disclaimer of Liability</h2>
<p>
DeFlock is provided "as is" without warranties of any kind. We do not guarantee the accuracy, reliability, or completeness of any map data or tagging guidance provided. DeFlock is not liable for any issues arising from:
</p>
<ul class="serif">
<li>The accuracy or use of data sourced from OpenStreetMap.</li>
<li>Contributions made to OpenStreetMap, including tagging errors or disputes.</li>
<li>Any unauthorized or prohibited use of our platform.</li>
</ul>
<h2>5. Intellectual Property</h2>
<p>
All content provided by DeFlock, including design, text, and logos, is the intellectual property of DeFlock unless otherwise noted. Users may not copy, distribute, or modify this content without prior written permission.
</p>
<p>
DeFlock's source code is licensed under the MIT License. For details, please refer to the <a target="_blank" href="https://github.com/FoggedLens/deflock?tab=MIT-1-ov-file#readme">LICENSE file</a>.
</p>
<h2>6. Termination of Use</h2>
<p>
We reserve the right to suspend or terminate access to DeFlock at our sole discretion if we believe a user has violated these Terms of Service.
</p>
<h2>7. Changes to the Terms of Service</h2>
<p>
DeFlock reserves the right to update or modify these Terms of Service at any time. Changes will be effective upon posting to our website. Continued use of DeFlock constitutes acceptance of the updated terms.
</p>
<h2>8. Contact Us</h2>
<p>
If you have any questions or concerns regarding these Terms of Service, please contact us at:
</p>
<p>
Email: <a href="mailto:contact@deflock.me">contact@deflock.me</a>
</p>
</v-container>
<Footer />
</template>
<script setup lang="ts">
import Footer from '@/components/layout/Footer.vue';
</script>
<style lang="css" scoped>
@import url('@/assets/typography.css');
</style>

View File

@@ -1,10 +1,11 @@
<template>
<v-container class="info-section" max-width="1000">
<p>
<v-img max-height="450" width="100%" cover src="/flock-camera.jpeg" />
</p>
<h2>What is an ALPR</h2>
<v-container fluid>
<v-row justify="center" class="hero-section-whatis text-center mb-4">
</v-row>
</v-container>
<v-container class="info-section">
<h1 class="mt-0">What is an ALPR?</h1>
<p>
Automated License Plate Readers (ALPRs) are cameras that capture images of all passing license plates, storing details like the car's location, date, and time. These cameras collect data on millions of vehicles—regardless 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.
</p>
@@ -12,111 +13,159 @@
<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>
<h2>Why Should You Be Concerned</h2>
<p class="mb-4">
<p class="mb-8 text-center">
ALPRs invade your privacy and violate your civil liberties. Here's how:
</p>
<dangers />
<h2 id="not-alpr" :class="{ highlighted: route.hash === '#not-alpr' }">What They Look Like</h2>
<v-carousel class="my-4" hide-delimiters>
<v-carousel-item
v-for="{ url, brand } in images"
:key="url"
aspect-ratio="1"
:src="url"
class="text-center"
>
<v-chip :rounded="false" color="warning" variant="elevated" size="x-large" style="position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);">
<span class="font-weight-bold">Brand: <span class="text-uppercase">{{ brand }}</span></span>
</v-chip>
</v-carousel-item>
</v-carousel>
<h2>What They Look Like</h2>
<p>
While <a href="https://en.wikipedia.org/wiki/Flock_Safety" target="_blank">Flock Safety</a> is a common brand of ALPRs in the US, there are several others, including <a href="https://en.wikipedia.org/wiki/Vigilant_Solutions" target="_blank">Vigilant Solutions</a>, owned by Motorola Solutions. Flock Safety ALPRs are easy to spot as they almost all look the same, typically mounted on poles with a solar panel on top. In rural areas, they are likely to be on standalone black poles, while in cities, they are more likely to be on existing utility or traffic poles. The cameras are often placed near intersections or on main roads at the edge of a city or town. Vigilant Solutions ALPRs, on the other hand, are typically mounted on traffic poles at or near intersections, with a distinctive white box mounted nearby.
</p>
<v-alert
density="compact"
class="mb-6 text-center"
variant="tonal"
color="rgb(18, 151, 195)"
>
<p>
<v-icon v-if="isMobile" size="x-large">mdi-gesture-tap</v-icon>
<v-icon v-else size="x-large">mdi-button-cursor</v-icon>
</p>
<p>
<b>{{ isMobile ? 'Tap' : 'Hover over' }}</b> an image below to identify the make.
<span v-if="!isMobile"><b>Click</b> an image to enlarge.</span>
</p>
</v-alert>
<h2>Not All Cameras are Law Enforcement ALPRs</h2>
<p>
Not all cameras near roads are ALPRs operated by law enforcement. Many people mistakenly assume that every traffic camera or intersection camera is an ALPR, but the reality is more nuanced. Here are some common types of cameras you might see near roads:
</p>
<v-row>
<v-col v-for="image in images" cols="12" sm="6" md="4">
<v-hover>
<template v-slot:default="{ isHovering, props }">
<v-img style="cursor: pointer;" @click="openFullScreenImage(image)" v-bind="props" cover :aspect-ratio="3/2" :src="image.url">
<transition name="fade">
<div class="scrim" v-show="isHovering">
<span class="scrim-text">{{ image.brand }}</span>
</div>
</transition>
</v-img>
</template>
</v-hover>
</v-col>
</v-row>
<div class="ml-4">
<h3>Traffic Cameras</h3>
<div class="flex-image">
<v-img rounded cover aspect-ratio="4/3" width="180" src="/traffic-camera.jpg" />
<p>Standard traffic cameras typically capture live video or images of intersections to monitor traffic flow and manage signals. They do not specifically focus on reading license plates or storing data long-term.</p>
</div>
<h3>Red Light Cameras/Speed Cameras</h3>
<div class="flex-image">
<v-img rounded cover aspect-ratio="4/3" width="180" src="/redlight-camera.jpg" />
<p>These cameras are set up to capture violations, such as running a red light or speeding. They may record plate numbers when a violation is detected, but they do not perform continuous surveillance or collect location data over time.</p>
</div>
<h3>Toll Cameras</h3>
<div class="flex-image">
<v-img rounded cover aspect-ratio="4/3" width="180" src="/toll-camera.jpg" />
<p>Cameras on toll roads are used to capture license plates for billing purposes. They are not designed to track vehicles or store data beyond the transaction.</p>
</div>
</div>
<h2>Common Brands of ALPRs</h2>
<ul class="serif mb-16">
<li>
<a href="https://www.flocksafety.com/devices/lpr" target="_blank">Flock Safety</a> - A leading provider of ALPR technology, known for their solar-powered cameras. This is the most common brand of ALPR in the US. Flock Safety cameras are used by police departments, HOAs, as well as private businesses such as hardware stores and hotels. One of the most appealing features of Flock cameras is the data sharing network, which allows law enforcement agencies to access data from other Flock cameras in the area. This means that even if your local police department doesn't have a Flock camera, they can still access data from other Flock cameras in the area.
</li>
<li>
<a href="https://www.motorolasolutions.com/en_us/video-security-access-control/license-plate-recognition-camera-systems.html" target="_blank">Vigilant Solutions</a> - Owned by Motorola Solutions, offering a range of ALPR products and services.
</li>
<li>
<a href="https://www.leonardocompany-us.com/lpr/elsag-fixed" target="_blank">ELSAG</a> - A subsidiary of Leonardo, specializing in ALPR technology.
</li>
<li>
<a href="https://neology.com/solutions/enforcement/" target="_blank">Neology</a> - Specializes in ALPR technology and tolling solutions.
</li>
</ul>
</v-container>
<v-dialog class="full-screen-image" v-model="showFullScreenImage">
<v-card style="overflow: hidden;">
<v-btn size="x-large" class="image-close-btn" flat icon @click="showFullScreenImage = false" color="transparent">
<v-icon :color="fullScreenImage.useDarkCloseButton ? 'black' : 'white'">mdi-close</v-icon>
</v-btn>
<v-img v-if="fullScreenImage" cover :aspect-ratio="3/2" :src="fullScreenImage.url" />
</v-card>
</v-dialog>
<Footer />
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { ref, type Ref } from 'vue';
import { useDisplay } from 'vuetify';
import Dangers from '@/components/Dangers.vue';
import Footer from '@/components/layout/Footer.vue';
const route = useRoute();
const { xs: isMobile } = useDisplay();
const flockImageCount = 6;
const vigilantImageCount = 3;
const showFullScreenImage = ref(false);
const fullScreenImage: Ref<any|undefined> = ref(undefined);
function openFullScreenImage(image: object) {
if (isMobile.value)
return;
fullScreenImage.value = image;
showFullScreenImage.value = true;
}
const images = [
...Array.from({ length: flockImageCount }, (_, i) => ({
url: `/flock-${i + 1}.jpg`,
brand: 'flock'
brand: 'flock',
useDarkCloseButton: false,
})),
...Array.from({ length: vigilantImageCount }, (_, i) => ({
url: `/vigilant-${i + 1}.jpg`,
brand: 'motorola'
brand: 'motorola',
useDarkCloseButton: true,
}))
];
</script>
<style scoped>
h2 {
margin-top: 2rem;
@import url('@/assets/typography.css');
h1, h2 {
text-align: center;
}
h3 {
margin-top: 1.25rem;
}
p {
margin-top: 0.5rem;
}
.flex-image {
.scrim {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
color: white;
font-size: 1.5rem;
font-weight: bold;
}
li {
margin-top: 0.5rem;
margin-left: 1rem;
.image-close-btn {
position: absolute;
top: 0;
right: 0;
color: white;
z-index: 100;
}
.highlighted {
background-color: yellow;
padding: 0.5rem;
border-radius: 0.25rem;
.scrim-text {
text-transform: uppercase;
}
.info-section {
background: var(--df-page-background-color);
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s;
}
.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>

View File

@@ -12,5 +12,9 @@ export default defineConfig({
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
host: '0.0.0.0',
port: 5173,
}
})