Sponsors and tags (#22)

* brand -> manufacturer

* clean up

* add donation page

* display manufacturer first, brand for backward compat
This commit is contained in:
Will Freeman
2024-12-16 12:29:24 -07:00
committed by GitHub
parent 6dff9cb2c8
commit cce969e6ba
15 changed files with 207 additions and 18 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# used for getting list of sponsors
GITHUB_TOKEN=github_pat_blah-blah-blah

2
.gitignore vendored
View File

@@ -49,3 +49,5 @@ serverless/*/src/*
!serverless/*/src/requirements.txt
# TODO: need a better way to handle python packages
.env

View File

@@ -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

View File

@@ -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,

View 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>

View File

@@ -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) =>

View 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}"))
}
}
}
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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);

View 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>&copy; {{ 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>