mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 15:02:45 +00:00
Sponsors and tags (#22)
* brand -> manufacturer * clean up * add donation page * display manufacturer first, brand for backward compat
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
# used for getting list of sponsors
|
||||
GITHUB_TOKEN=github_pat_blah-blah-blah
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,3 +49,5 @@ serverless/*/src/*
|
||||
!serverless/*/src/requirements.txt
|
||||
|
||||
# TODO: need a better way to handle python packages
|
||||
|
||||
.env
|
||||
|
||||
@@ -12,3 +12,5 @@ services:
|
||||
retries: 3 # Number of retries before marking as unhealthy
|
||||
start_period: 10s # Time to wait before starting health checks
|
||||
stdin_open: true
|
||||
environment:
|
||||
- GITHUB_TOKEN
|
||||
|
||||
@@ -14,6 +14,10 @@ lazy val root = (project in file("."))
|
||||
val PekkoVersion = "1.0.3"
|
||||
val PekkoHttpVersion = "1.0.1"
|
||||
libraryDependencies ++= Seq(
|
||||
"ch.qos.logback" % "logback-classic" % "1.5.6",
|
||||
"org.slf4j" % "slf4j-api" % "2.0.12",
|
||||
"com.auth0" % "jwks-rsa" % "0.22.1",
|
||||
"com.github.jwt-scala" % "jwt-json-common_native0.4_2.12" % "10.0.1",
|
||||
"org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion,
|
||||
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
|
||||
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
|
||||
|
||||
12
shotgun/src/main/resources/logback.xml
Normal file
12
shotgun/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -17,9 +17,12 @@ import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import scala.concurrent.ExecutionContextExecutor
|
||||
import scala.io.StdIn
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
object ShotgunServer {
|
||||
|
||||
val logger = LoggerFactory.getLogger(getClass)
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
|
||||
implicit val system: ActorSystem = ActorSystem("my-system")
|
||||
@@ -28,6 +31,7 @@ object ShotgunServer {
|
||||
|
||||
val client = new services.OverpassClient()
|
||||
val nominatim = new services.NominatimClient()
|
||||
val githubClient = new services.GithubClient()
|
||||
|
||||
// CORS
|
||||
val allowedOrigins = List(
|
||||
@@ -72,6 +76,13 @@ object ShotgunServer {
|
||||
}
|
||||
}
|
||||
},
|
||||
path("sponsors" / "github") {
|
||||
get {
|
||||
onSuccess(githubClient.getSponsors("frillweeman")) { json =>
|
||||
complete(json)
|
||||
}
|
||||
}
|
||||
},
|
||||
path("oauth2" / "callback") {
|
||||
get {
|
||||
parameters(Symbol("code").?) { (code) =>
|
||||
|
||||
68
shotgun/src/main/scala/services/GithubClient.scala
Normal file
68
shotgun/src/main/scala/services/GithubClient.scala
Normal file
@@ -0,0 +1,68 @@
|
||||
package services
|
||||
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.javadsl.model.headers.{Authorization, HttpCredentials, UserAgent}
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.model.{HttpMethods, HttpRequest, StatusCodes}
|
||||
import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal
|
||||
import spray.json.JsValue
|
||||
import spray.json._
|
||||
import org.apache.pekko.http.scaladsl.model.ContentTypes
|
||||
import org.apache.pekko.http.scaladsl.model.HttpEntity
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future}
|
||||
|
||||
class GithubClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) {
|
||||
val logger = LoggerFactory.getLogger(getClass)
|
||||
val graphQLEndpoint = "https://api.github.com/graphql"
|
||||
private val githubApiToken = sys.env("GITHUB_TOKEN")
|
||||
|
||||
def getSponsors(username: String): Future[JsArray] = {
|
||||
|
||||
val query = s"""
|
||||
|query {
|
||||
| user(login: "$username") {
|
||||
| sponsorshipsAsMaintainer(first: 100) {
|
||||
| nodes {
|
||||
| sponsor {
|
||||
| login
|
||||
| name
|
||||
| avatarUrl
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
|}
|
||||
|""".stripMargin.replace("\n", " ").replace("\"", "\\\"")
|
||||
|
||||
val jsonRequest = s"""{"query": "$query", "variables": ""}"""
|
||||
val jsonEntity = HttpEntity(ContentTypes.`application/json`, jsonRequest)
|
||||
val request = HttpRequest(
|
||||
headers = List(
|
||||
UserAgent.create("Shotgun"),
|
||||
Authorization.create(HttpCredentials.create("Bearer", githubApiToken))
|
||||
),
|
||||
method = HttpMethods.POST,
|
||||
uri = graphQLEndpoint,
|
||||
entity = jsonEntity
|
||||
)
|
||||
|
||||
Http().singleRequest(request).flatMap { response =>
|
||||
response.status match {
|
||||
case StatusCodes.OK =>
|
||||
Unmarshal(response.entity).to[String].map { jsonString =>
|
||||
jsonString.parseJson.asJsObject
|
||||
.fields("data").asJsObject
|
||||
.fields("user").asJsObject
|
||||
.fields("sponsorshipsAsMaintainer")
|
||||
.asJsObject.fields("nodes")
|
||||
.asInstanceOf[JsArray]
|
||||
}
|
||||
case _ =>
|
||||
response.discardEntityBytes()
|
||||
Future.failed(new Exception(s"Failed to get sponsors: ${response.status}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import pekko.http.scaladsl.Http
|
||||
import pekko.http.scaladsl.model._
|
||||
import pekko.http.scaladsl.unmarshalling.Unmarshal
|
||||
import spray.json._
|
||||
import DefaultJsonProtocol._
|
||||
import scala.concurrent.{ExecutionContextExecutor, Future}
|
||||
|
||||
case class BoundingBox(minLat: Double, minLng: Double, maxLat: Double, maxLng: Double)
|
||||
|
||||
BIN
webapp/public/torches.webp
Normal file
BIN
webapp/public/torches.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
@@ -25,7 +25,7 @@ const metaItems = [
|
||||
{ title: 'Discord', icon: 'mdi-chat-processing-outline', 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', href: 'https://github.com/sponsors/frillweeman'},
|
||||
{ title: 'Donate', icon: 'mdi-heart', to: '/donate'},
|
||||
];
|
||||
const drawer = ref(false)
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-icon start>mdi-domain</v-icon> <b>
|
||||
<span v-if="alpr.tags.brand">
|
||||
<span v-if="alpr.tags.manufacturer">
|
||||
{{ alpr.tags.manufacturer }}
|
||||
</span>
|
||||
<span v-else-if="alpr.tags.brand">
|
||||
{{ alpr.tags.brand }}
|
||||
</span>
|
||||
<span v-else>
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
<template>
|
||||
<v-row style="align-items: center; margin-top: 1.25rem;">
|
||||
<v-col cols="12" sm="6">
|
||||
<!-- <v-select
|
||||
v-model="selectedBrand"
|
||||
return-object
|
||||
:items="alprBrands"
|
||||
item-title="name"
|
||||
item-value="wikidata"
|
||||
label="Select a company"
|
||||
outlined
|
||||
/> -->
|
||||
<h2 class="text-center mb-4">Choose Brand</h2>
|
||||
<v-row>
|
||||
<v-col v-for="brand in alprBrands" :key="brand.wikidata" cols="6">
|
||||
@@ -26,10 +17,6 @@
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<!-- <v-col cols="12" sm="4">
|
||||
<v-img rounded cover aspect-ratio="1" width="220" :src="selectedBrand.exampleImage" />
|
||||
</v-col> -->
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<h3 class="text-center">{{ selectedBrand.nickname }}</h3>
|
||||
<DFCode>
|
||||
@@ -39,8 +26,8 @@
|
||||
camera:type=fixed<br>
|
||||
surveillance=public<br>
|
||||
surveillance:zone=traffic<br>
|
||||
brand=<span class="highlight">{{ selectedBrand.name }}</span><br>
|
||||
brand:wikidata=<span class="highlight">{{ selectedBrand.wikidata }}</span><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>
|
||||
|
||||
@@ -67,6 +67,11 @@ const router = createRouter({
|
||||
name: 'qr-landing',
|
||||
component: () => import('../views/QRLandingView.vue')
|
||||
},
|
||||
{
|
||||
path: '/donate',
|
||||
name: 'donate',
|
||||
component: () => import('../views/Donate.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
|
||||
@@ -65,6 +65,11 @@ export const getALPRs = async (boundingBox: BoundingBox) => {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const getSponsors = async () => {
|
||||
const response = await apiService.get("/sponsors/github");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const getALPRCounts = async () => {
|
||||
const s3Url = "https://deflock-clusters.s3.us-east-1.amazonaws.com/alpr-counts.json";
|
||||
const response = await apiService.get(s3Url);
|
||||
|
||||
89
webapp/src/views/Donate.vue
Normal file
89
webapp/src/views/Donate.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<v-container class="sponsor-page">
|
||||
<!-- Hero Section -->
|
||||
<v-row justify="center" class="hero-section-sponsor text-center mb-4">
|
||||
<v-col cols="12" md="8">
|
||||
<h1 class="mb-4">Join Us in Protecting Privacy</h1>
|
||||
<p class="mb-4">
|
||||
DeFlock empowers individuals to understand and combat the rise of Automatic License Plate Readers (ALPRs). Your support helps us spread awareness, maintain infrastructure, and advocate for privacy rights.
|
||||
</p>
|
||||
<v-btn href="https://github.com/sponsors/frillweeman" target="_blank" color="rgb(18, 151, 195)" class="mt-4">Donate Now</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 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>
|
||||
<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-avatar size="64px" class="mb-3">
|
||||
<v-img :src="sponsor.avatarUrl" :alt="sponsor.name" />
|
||||
</v-avatar>
|
||||
<p>{{ sponsor.name ?? sponsor.login }}</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<v-footer class="text-center mt-8">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<p>© {{ new Date().getFullYear() }} DeFlock. All rights reserved.</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, type Ref } from "vue";
|
||||
import { getSponsors } from "@/services/apiService";
|
||||
|
||||
interface Sponsor {
|
||||
login: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
const sponsors: Ref<Sponsor[]> = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
getSponsors()
|
||||
.then((data) => {
|
||||
sponsors.value = data.map((s: any) => s.sponsor);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hero-section-sponsor {
|
||||
background: url('/torches.webp') no-repeat center center;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
padding: 100px 0 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-section-sponsor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-section-sponsor > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user