diff --git a/apps/timsexperiments/.env.local b/apps/timsexperiments/.env.local
index 9f02cd7..553fcdf 100644
--- a/apps/timsexperiments/.env.local
+++ b/apps/timsexperiments/.env.local
@@ -1 +1,2 @@
-PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views
\ No newline at end of file
+# PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views
+PUBLIC_VIEW_COUNTER_API=https://dev.views.timsexperiments.foo/
\ No newline at end of file
diff --git a/apps/timsexperiments/package.json b/apps/timsexperiments/package.json
index f97a3df..4fbb2d4 100644
--- a/apps/timsexperiments/package.json
+++ b/apps/timsexperiments/package.json
@@ -27,7 +27,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@timsexperiments/theme": "^0.0.0",
- "@timsexperiments/three-rubiks-cube": "^0.0.3",
+ "@timsexperiments/three-rubiks-cube": "^0.0.5",
"@timsexperiments/views-client": "workspace:*",
"@types/hast": "^3.0.4",
"@types/react": "^18.2.47",
diff --git a/apps/timsexperiments/src/components/nav/Nav.astro b/apps/timsexperiments/src/components/nav/Nav.astro
index f597c06..2660add 100644
--- a/apps/timsexperiments/src/components/nav/Nav.astro
+++ b/apps/timsexperiments/src/components/nav/Nav.astro
@@ -51,4 +51,4 @@ import { Home, Compass, FlaskConical, Mail, UserRoundPlus } from 'lucide-react';
}
-
+
diff --git a/apps/timsexperiments/src/env.d.ts b/apps/timsexperiments/src/env.d.ts
index 99a3b5c..d2bfbed 100644
--- a/apps/timsexperiments/src/env.d.ts
+++ b/apps/timsexperiments/src/env.d.ts
@@ -1,3 +1,11 @@
///
///
///
+
+interface ImportMetaEnv {
+ readonly PUBLIC_VIEW_COUNTER_API: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/apps/timsexperiments/src/components/nav/nav.ts b/apps/timsexperiments/src/scripts/nav.ts
similarity index 92%
rename from apps/timsexperiments/src/components/nav/nav.ts
rename to apps/timsexperiments/src/scripts/nav.ts
index 253deb0..56ae5c5 100644
--- a/apps/timsexperiments/src/components/nav/nav.ts
+++ b/apps/timsexperiments/src/scripts/nav.ts
@@ -36,11 +36,6 @@ const observer = new IntersectionObserver(
if (entry.isIntersecting && !anyIntersecting) {
anyIntersecting = true;
navigator.classList.add('active');
- console.log(
- 'scrolling the navigator for',
- entry.target.id,
- 'into view'
- );
navigator.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} else {
navigator.classList.remove('active');
diff --git a/apps/view-counter/src/index.ts b/apps/view-counter/src/index.ts
index 7bc0d82..9874429 100644
--- a/apps/view-counter/src/index.ts
+++ b/apps/view-counter/src/index.ts
@@ -4,9 +4,9 @@ import { ViewsHandler } from './views';
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
- * - Run `bun run dev` in your terminal to start a development server
+ * - Run `pnpm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
- * - Run `bun run deploy` to publish your worker
+ * - Run `pnpm run deploy` to publish your worker
*
* Learn more at https://developers.cloudflare.com/workers/
*/
diff --git a/apps/view-counter/src/views/index.ts b/apps/view-counter/src/views/index.ts
index a87f737..9f534d5 100644
--- a/apps/view-counter/src/views/index.ts
+++ b/apps/view-counter/src/views/index.ts
@@ -12,7 +12,6 @@ export class ViewsHandler implements RouteHandler {
env: Env,
baseHeaders: Record,
) {
- console.log(env.TURSO_AUTH_TOKEN);
const db = createDb({
url: env.TURSO_URL,
authToken: env.TURSO_AUTH_TOKEN,
@@ -66,8 +65,15 @@ export class ViewsHandler implements RouteHandler {
private async POST(): Promise {
const clientIP = this.request.headers.get('CF-Connecting-IP')!;
- const view = (await this.request.json()) as Omit;
- await this.db.add({ ipAddress: clientIP, ...view });
+ const view = (await this.request.json()) as Omit;
+ const { page, ...rest } = view;
+ let pageUrl: URL;
+ try {
+ pageUrl = new URL(page);
+ } catch {
+ return badRequestResponse({ message: 'Page must be a valid fully qualified URL.', headers: this.baseHeaders });
+ }
+ await this.db.add({ ipAddress: clientIP, ...rest, page: pageUrl.href, path: pageUrl.pathname });
return responseNoContent({ headers: this.baseHeaders });
}
}
diff --git a/packages/view-storage/package.json b/packages/view-storage/package.json
index c802ecd..18a8d8d 100644
--- a/packages/view-storage/package.json
+++ b/packages/view-storage/package.json
@@ -3,6 +3,10 @@
"module": "src/index.ts",
"types": "src/index.ts",
"type": "module",
+ "dependencies": {
+ "@libsql/client": "^0.6.0",
+ "drizzle-orm": "^0.30.8"
+ },
"devDependencies": {
"@types/bun": "latest",
"dotenv": "^16.4.5",
@@ -10,9 +14,5 @@
},
"peerDependencies": {
"typescript": "^5.0.0"
- },
- "dependencies": {
- "@libsql/client": "^0.6.0",
- "drizzle-orm": "^0.30.8"
}
}
diff --git a/packages/view-storage/src/client.ts b/packages/view-storage/src/client.ts
index 666dab6..3ed9420 100644
--- a/packages/view-storage/src/client.ts
+++ b/packages/view-storage/src/client.ts
@@ -2,14 +2,14 @@ import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
export type CreateDbOptions = {
+ authToken: string;
url: string;
- authToken?: string;
};
-export function createDb({ url, authToken }: CreateDbOptions) {
+export function createDb({ authToken, url }: CreateDbOptions) {
const client = createClient({
- url,
authToken,
+ url,
});
return drizzle(client);
}
diff --git a/packages/view-storage/src/schema.ts b/packages/view-storage/src/schema.ts
index 85bdd72..76fd266 100644
--- a/packages/view-storage/src/schema.ts
+++ b/packages/view-storage/src/schema.ts
@@ -13,6 +13,7 @@ export const views = sqliteTable(
{
id: integer("id").primaryKey(),
page: text("page", { length: 256 }).notNull(),
+ path: text("path", { length: 256 }).notNull(),
ipAddress: text("ip_address", { length: 40 }).notNull(),
viewedAt: integer("viewed_at", { mode: "timestamp_ms" })
.default(sql`CURRENT_TIMESTAMP`)
diff --git a/packages/views-client/package.json b/packages/views-client/package.json
index 0f81d77..547895c 100644
--- a/packages/views-client/package.json
+++ b/packages/views-client/package.json
@@ -9,5 +9,8 @@
},
"peerDependencies": {
"typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "valibot": "^0.32.0"
}
}
diff --git a/packages/views-client/src/index.ts b/packages/views-client/src/index.ts
index 3d93102..0489c4c 100644
--- a/packages/views-client/src/index.ts
+++ b/packages/views-client/src/index.ts
@@ -1,16 +1,23 @@
import { type View } from "@timsexperiments/view-storage";
+import * as v from "valibot";
-export type GetViewsOptions = {
- page: string;
-};
+const GetViewsSchema = v.object({
+ page: v.pipe(v.string(), v.url()),
+});
-export type AddViewOpitons = {
- page: string;
-};
+export type GetViewsOptions = v.InferOutput;
-export type ViewsClientOptions = {
- host: string;
-};
+const AddViewSchema = v.object({
+ page: v.pipe(v.string(), v.url()),
+});
+
+export type AddViewOpitons = v.InferOutput;
+
+const ViewsClientOptionsSchema = v.object({
+ host: v.pipe(v.string(), v.url()),
+});
+
+export type ViewsClientOptions = v.InferOutput;
/**
* Client for interacting with the timsexperiments views api.
@@ -18,7 +25,15 @@ export type ViewsClientOptions = {
export class ViewsClient {
private readonly url;
- constructor({ host }: ViewsClientOptions) {
+ constructor(options: ViewsClientOptions) {
+ const parsed = v.safeParse(ViewsClientOptionsSchema, options);
+ if (!parsed.success) {
+ throw new ViewClientError(
+ "Invalid options provided: " + JSON.stringify(parsed.issues)
+ );
+ }
+
+ const { host } = parsed.output;
this.url = new URL(host);
}
@@ -29,7 +44,14 @@ export class ViewsClient {
* @param {string} options.page - The page for which to retrieve views.
* @returns {Promise} - A promise that resolves to the retrieved views.
*/
- async getViews({ page }: GetViewsOptions) {
+ async getViews(options: GetViewsOptions) {
+ const parsed = v.safeParse(GetViewsSchema, options);
+ if (!parsed.success) {
+ throw new ViewClientError(
+ "Invalid options provided: " + JSON.stringify(parsed.issues)
+ );
+ }
+ const { page } = parsed.output;
const url = this.viewsUrl;
url.searchParams.append("page", page);
const response = await fetch(url.href);
@@ -43,12 +65,20 @@ export class ViewsClient {
* @param {string} options.page - The page for which to add a view.
* @returns {Promise} - A promise that resolves when the view is added successfully.
*/
- async addView({ page }: AddViewOpitons) {
+ async addView(options: AddViewOpitons) {
+ const parsed = v.safeParse(AddViewSchema, options);
+ if (!parsed.success) {
+ throw new ViewClientError(
+ "Invalid options provided: " + JSON.stringify(parsed.issues)
+ );
+ }
+ const { page } = parsed.output;
+ const pageUrl = new URL(page);
const url = this.viewsUrl;
await fetch(url.href, {
method: "POST",
body: JSON.stringify({
- page,
+ page: pageUrl.href,
}),
headers: {
"Content-Type": "application/json",
@@ -64,3 +94,10 @@ export class ViewsClient {
}
export default ViewsClient;
+
+export class ViewClientError extends Error {
+ constructor(message: string, options?: ErrorOptions) {
+ super(message, options);
+ this.name = "ViewClientError";
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a71b91..d6866c2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -73,8 +73,8 @@ importers:
specifier: ^0.0.0
version: 0.0.0
'@timsexperiments/three-rubiks-cube':
- specifier: ^0.0.3
- version: 0.0.3(three-mesh-bvh@0.7.5(three@0.165.0))
+ specifier: ^0.0.5
+ version: 0.0.5(three-mesh-bvh@0.7.5(three@0.165.0))
'@timsexperiments/views-client':
specifier: workspace:*
version: link:../../packages/views-client
@@ -219,6 +219,9 @@ importers:
typescript:
specifier: ^5.0.0
version: 5.4.5
+ valibot:
+ specifier: ^0.32.0
+ version: 0.32.0
devDependencies:
'@timsexperiments/view-storage':
specifier: workspace:*
@@ -1810,8 +1813,8 @@ packages:
resolution: {integrity: sha512-UdwWfGJPb/5LFIM+g8DNw7qvxz2pI5chexvA6dpJYTBkOUA07yALC++/y6Oz9lxNgIhAAzpbX+6vrsNKleXsQA==}
engines: {vscode: ^1.88.0}
- '@timsexperiments/three-rubiks-cube@0.0.3':
- resolution: {integrity: sha512-LeRPFyOoFEtO/YlpeuvR6yo5vHj0zFJi3y8Xa0gd4T8Bt2fd3h30xDVKBMakCGeWZKAOUsYfhIyoPd6ghQg6mw==}
+ '@timsexperiments/three-rubiks-cube@0.0.5':
+ resolution: {integrity: sha512-6rMOe7xA8x1Wuk4gViWjGTFAp1SLTVNv9C7fe9sASXe7E1l8g/TYx3VcM+0vz3J0oEszmlgZ5EEnbH+fC2VwXQ==}
'@timsexperiments/ts-plugin-workers-wasm@0.0.2':
resolution: {integrity: sha512-REgfwnM/Cgc4AdBszcUOB5et2nDmCiyUeyUZx0Dw/aSvANeZgreRxl+1YfXO1+DPonCmJi4GXOdtE/BEkidKGw==}
@@ -4147,6 +4150,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+ valibot@0.32.0:
+ resolution: {integrity: sha512-FXBnJl4bNOmeg7lQv+jfvo/wADsRBN8e9C3r+O77Re3dEnDma8opp7p4hcIbF7XJJ30h/5SVohdjer17/sHOsQ==}
+
vfile-location@5.0.2:
resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==}
@@ -5740,7 +5746,7 @@ snapshots:
'@timsexperiments/theme@0.0.0': {}
- '@timsexperiments/three-rubiks-cube@0.0.3(three-mesh-bvh@0.7.5(three@0.165.0))':
+ '@timsexperiments/three-rubiks-cube@0.0.5(three-mesh-bvh@0.7.5(three@0.165.0))':
dependencies:
three: 0.165.0
three-bvh-csg: 0.0.16(three-mesh-bvh@0.7.5(three@0.165.0))(three@0.165.0)
@@ -8559,6 +8565,8 @@ snapshots:
util-deprecate@1.0.2: {}
+ valibot@0.32.0: {}
+
vfile-location@5.0.2:
dependencies:
'@types/unist': 3.0.2