Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add view counter #6

Merged
merged 1 commit into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/timsexperiments/.env.local
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views
# PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views
PUBLIC_VIEW_COUNTER_API=https://dev.views.timsexperiments.foo/
2 changes: 1 addition & 1 deletion apps/timsexperiments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/timsexperiments/src/components/nav/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ import { Home, Compass, FlaskConical, Mail, UserRoundPlus } from 'lucide-react';
}
</style>

<script src="./nav.ts"></script>
<script src="@/scripts/nav.ts"></script>
8 changes: 8 additions & 0 deletions apps/timsexperiments/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/// <reference path="../.astro/db-types.d.ts" />
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

interface ImportMetaEnv {
readonly PUBLIC_VIEW_COUNTER_API: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions apps/view-counter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
*/
Expand Down
12 changes: 9 additions & 3 deletions apps/view-counter/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export class ViewsHandler implements RouteHandler {
env: Env,
baseHeaders: Record<string, string>,
) {
console.log(env.TURSO_AUTH_TOKEN);
const db = createDb({
url: env.TURSO_URL,
authToken: env.TURSO_AUTH_TOKEN,
Expand Down Expand Up @@ -66,8 +65,15 @@ export class ViewsHandler implements RouteHandler {

private async POST(): Promise<Response> {
const clientIP = this.request.headers.get('CF-Connecting-IP')!;
const view = (await this.request.json()) as Omit<View, 'ipAddress'>;
await this.db.add({ ipAddress: clientIP, ...view });
const view = (await this.request.json()) as Omit<View, 'ipAddress' | 'path'>;
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 });
}
}
8 changes: 4 additions & 4 deletions packages/view-storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
"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",
"drizzle-kit": "^0.20.17"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@libsql/client": "^0.6.0",
"drizzle-orm": "^0.30.8"
}
}
6 changes: 3 additions & 3 deletions packages/view-storage/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/view-storage/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
3 changes: 3 additions & 0 deletions packages/views-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"valibot": "^0.32.0"
}
}
63 changes: 50 additions & 13 deletions packages/views-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
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<typeof GetViewsSchema>;

export type ViewsClientOptions = {
host: string;
};
const AddViewSchema = v.object({
page: v.pipe(v.string(), v.url()),
});

export type AddViewOpitons = v.InferOutput<typeof AddViewSchema>;

const ViewsClientOptionsSchema = v.object({
host: v.pipe(v.string(), v.url()),
});

export type ViewsClientOptions = v.InferOutput<typeof ViewsClientOptionsSchema>;

/**
* Client for interacting with the timsexperiments views api.
*/
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);
}

Expand All @@ -29,7 +44,14 @@ export class ViewsClient {
* @param {string} options.page - The page for which to retrieve views.
* @returns {Promise<View>} - 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);
Expand All @@ -43,12 +65,20 @@ export class ViewsClient {
* @param {string} options.page - The page for which to add a view.
* @returns {Promise<void>} - 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",
Expand All @@ -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";
}
}
18 changes: 13 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading