diff --git a/frontend/bun.lockb b/frontend/bun.lockb index ac4a2ac..5861839 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/drizzle/0002_robust_the_executioner.sql b/frontend/drizzle/0002_robust_the_executioner.sql new file mode 100644 index 0000000..dedfe46 --- /dev/null +++ b/frontend/drizzle/0002_robust_the_executioner.sql @@ -0,0 +1 @@ +ALTER TABLE "visitor" ADD COLUMN "city" varchar(255) NOT NULL; \ No newline at end of file diff --git a/frontend/drizzle/meta/0002_snapshot.json b/frontend/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..8addc8d --- /dev/null +++ b/frontend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,187 @@ +{ + "id": "8f8ae49c-43bf-4e07-a30d-383e2b767524", + "prevId": "548a0489-b083-464f-800c-4dee6f1ff161", + "version": "5", + "dialect": "pg", + "tables": { + "session": { + "name": "session", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "shortener": { + "name": "shortener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "link": { + "name": "link", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "visitor": { + "name": "visitor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "shortener_id": { + "name": "shortener_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "country_code": { + "name": "country_code", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json index 7f6eb6d..638942c 100644 --- a/frontend/drizzle/meta/_journal.json +++ b/frontend/drizzle/meta/_journal.json @@ -1,20 +1,27 @@ { - "version": "5", - "dialect": "pg", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1699851315914, - "tag": "0000_nebulous_energizer", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1700134783172, - "tag": "0001_regular_microchip", - "breakpoints": true - } - ] -} + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1699851315914, + "tag": "0000_nebulous_energizer", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1700134783172, + "tag": "0001_regular_microchip", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1700882455122, + "tag": "0002_robust_the_executioner", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/frontend/src/lib/components/ui/tabs/index.ts b/frontend/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000..968804c --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import { Tabs as TabsPrimitive } from "bits-ui"; +import Content from "./tabs-content.svelte"; +import List from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +const Root = TabsPrimitive.Root; + +export { + Root, + Content, + List, + Trigger, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger +}; diff --git a/frontend/src/lib/components/ui/tabs/tabs-content.svelte b/frontend/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000..3866292 --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/tabs/tabs-list.svelte b/frontend/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000..8905c77 --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000..d8cac33 --- /dev/null +++ b/frontend/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/lib/db/schema.ts b/frontend/src/lib/db/schema.ts index b32a040..f3c4832 100644 --- a/frontend/src/lib/db/schema.ts +++ b/frontend/src/lib/db/schema.ts @@ -51,6 +51,7 @@ export const visitor = pgTable('visitor', { length: 255, }).notNull(), country: varchar('country', { length: 255 }).notNull(), + city: varchar('city', { length: 255 }).notNull(), }) export const visitorRelations = relations(visitor, ({ one }) => ({ diff --git a/frontend/src/routes/(app)/links/[id]/+page.server.ts b/frontend/src/routes/(app)/links/[id]/+page.server.ts index 8d878d9..6be7404 100644 --- a/frontend/src/routes/(app)/links/[id]/+page.server.ts +++ b/frontend/src/routes/(app)/links/[id]/+page.server.ts @@ -19,6 +19,10 @@ export const load = (async (event) => { }, }) + if (!shortener) { + throw redirect(303, '/') + } + const visitor = await db .select({ count: sql`cast(count(*) as int)`, @@ -27,9 +31,28 @@ export const load = (async (event) => { .from(visitorSchema) .groupBy(sql`to_char(${visitorSchema.createdAt}, 'MM')`) - if (!shortener) { - throw redirect(303, '/') - } + const visitorByCountry = await db + .select({ + count: sql`cast(count(*) as int)`, + country: visitorSchema.country, + code: visitorSchema.countryCode, + }) + .from(visitorSchema) + .groupBy(visitorSchema.country, visitorSchema.countryCode) + + const visitorByCity = await db + .select({ + count: sql`cast(count(*) as int)`, + country: visitorSchema.country, + code: visitorSchema.countryCode, + city: visitorSchema.city, + }) + .from(visitorSchema) + .groupBy( + visitorSchema.country, + visitorSchema.countryCode, + visitorSchema.city, + ) - return { shortener, visitor } + return { shortener, visitor, visitorByCountry, visitorByCity } }) satisfies PageServerLoad diff --git a/frontend/src/routes/(app)/links/[id]/+page.svelte b/frontend/src/routes/(app)/links/[id]/+page.svelte index 03e3b4d..acbb0b0 100644 --- a/frontend/src/routes/(app)/links/[id]/+page.svelte +++ b/frontend/src/routes/(app)/links/[id]/+page.svelte @@ -2,6 +2,7 @@ import type { PageData } from './$types' import { Separator } from '$lib/components/ui/separator' import * as Card from '$lib/components/ui/card' + import * as Tabs from '$lib/components/ui/tabs' import type { ApexOptions } from 'apexcharts' import { mode } from 'mode-watcher' import { onMount } from 'svelte' @@ -84,15 +85,15 @@ if (container) { container.innerHTML = '' } - var chart = new apexChart(container, options) + var chart = new ApexChart(container, options) chart.render() } - $: container && apexChart && renderChart(options) + $: container && ApexChart && renderChart(options) - let apexChart: typeof ApexCharts + let ApexChart: typeof ApexCharts onMount(async () => { - apexChart = (await import('apexcharts')).default + ApexChart = (await import('apexcharts')).default }) @@ -112,4 +113,48 @@ + + + + + Visitors + Visitors by Country/City + + + Country + City + + + + + {#each data.visitorByCountry as visitorByCountry} + + + + {visitorByCountry.country} + + {visitorByCountry.count} + + {/each} + + + {#each data.visitorByCity as visitorByCity} + + + + {visitorByCity.city} + + {visitorByCity.count} + + {/each} + + + + diff --git a/redirect/src/index.ts b/redirect/src/index.ts index b586425..31cc042 100644 --- a/redirect/src/index.ts +++ b/redirect/src/index.ts @@ -31,6 +31,7 @@ app.get( country: geolocation.data.location.country.name as string, country_code: geolocation.data.location.country .alpha2 as string, + city: geolocation.data.location.city.name as string, } await db.insertInto('visitor').values(visitor_data).execute() diff --git a/redirect/src/types.ts b/redirect/src/types.ts index a405199..ba2e849 100644 --- a/redirect/src/types.ts +++ b/redirect/src/types.ts @@ -30,6 +30,7 @@ export interface VisitorTable { shortener_id: number country: string country_code: string + city: string created_at: ColumnType }