Skip to content

Commit

Permalink
feat(www): implement event tracking (shadcn-ui#218)
Browse files Browse the repository at this point in the history
* feat(www): implement event tracking

* fix(www): always show copy button

* fix(www): update align props for copy button
  • Loading branch information
shadcn authored Apr 20, 2023
1 parent bfc6614 commit 5e91575
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 44 deletions.
56 changes: 41 additions & 15 deletions apps/www/components/copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react"
import { DropdownMenuTriggerProps } from "@radix-ui/react-dropdown-menu"
import { NpmCommands } from "types/unist"

import { Event, trackEvent } from "@/lib/events"
import { cn } from "@/lib/utils"
import {
DropdownMenu,
Expand All @@ -16,19 +17,21 @@ import { Icons } from "@/components/icons"
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
value: string
src?: string
event?: Event["name"]
}

async function copyToClipboardWithMeta(
value: string,
meta?: Record<string, unknown>
) {
async function copyToClipboardWithMeta(value: string, event?: Event) {
navigator.clipboard.writeText(value)
if (event) {
trackEvent(event)
}
}

export function CopyButton({
value,
className,
src,
event,
...props
}: CopyButtonProps) {
const [hasCopied, setHasCopied] = React.useState(false)
Expand All @@ -46,9 +49,17 @@ export function CopyButton({
className
)}
onClick={() => {
copyToClipboardWithMeta(value, {
component: src,
})
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
Expand Down Expand Up @@ -135,10 +146,19 @@ export function CopyNpmCommandButton({
}, 2000)
}, [hasCopied])

const copyCommand = React.useCallback((value: string) => {
copyToClipboardWithMeta(value)
setHasCopied(true)
}, [])
const copyCommand = React.useCallback(
(value: string, pm: "npm" | "pnpm" | "yarn") => {
copyToClipboardWithMeta(value, {
name: "copy_npm_command",
properties: {
command: value,
pm,
},
})
setHasCopied(true)
},
[]
)

return (
<DropdownMenu>
Expand All @@ -156,16 +176,22 @@ export function CopyNpmCommandButton({
)}
<span className="sr-only">Copy</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => copyCommand(commands.__npmCommand__)}>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => copyCommand(commands.__npmCommand__, "npm")}
>
<Icons.npm className="mr-2 h-4 w-4" />
<span>npm</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyCommand(commands.__yarnCommand__)}>
<DropdownMenuItem
onClick={() => copyCommand(commands.__yarnCommand__, "yarn")}
>
<Icons.yarn className="mr-2 h-4 w-4" />
<span>yarn</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyCommand(commands.__pnpmCommand__)}>
<DropdownMenuItem
onClick={() => copyCommand(commands.__pnpmCommand__, "pnpm")}
>
<Icons.pnpm className="mr-2 h-4 w-4" />
<span>pnpm</span>
</DropdownMenuItem>
Expand Down
4 changes: 4 additions & 0 deletions apps/www/components/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from "next/image"
import { useMDXComponent } from "next-contentlayer/hooks"
import { NpmCommands } from "types/unist"

import { Event } from "@/lib/events"
import { cn } from "@/lib/utils"
import {
Accordion,
Expand Down Expand Up @@ -157,11 +158,13 @@ const components = {
__yarnCommand__,
__withMeta__,
__src__,
__event__,
...props
}: React.HTMLAttributes<HTMLPreElement> & {
__rawString__?: string
__withMeta__?: boolean
__src__?: string
__event__?: Event["name"]
} & NpmCommands) => {
return (
<>
Expand All @@ -176,6 +179,7 @@ const components = {
<CopyButton
value={__rawString__}
src={__src__}
event={__event__}
className={cn("absolute right-4 top-4", __withMeta__ && "top-16")}
/>
)}
Expand Down
14 changes: 14 additions & 0 deletions apps/www/contentlayer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ export default makeSource({
return
}

if (codeEl.data?.meta) {
// Extract event from meta and pass it down the tree.
const regex = /event="([^"]*)"/
const match = codeEl.data?.meta.match(regex)
if (match) {
node.__event__ = match ? match[1] : null
codeEl.data.meta = codeEl.data.meta.replace(regex, "")
}
}

node.__rawString__ = codeEl.children?.[0].value
node.__src__ = node.properties?.__src__
}
Expand Down Expand Up @@ -147,6 +157,10 @@ export default makeSource({
if (node.__src__) {
preElement.properties["__src__"] = node.__src__
}

if (node.__event__) {
preElement.properties["__event__"] = node.__event__
}
}
})
},
Expand Down
24 changes: 24 additions & 0 deletions apps/www/lib/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import va from "@vercel/analytics"
import { z } from "zod"

const eventSchema = z.object({
name: z.enum([
"copy_npm_command",
"copy_usage_import_code",
"copy_usage_code",
"copy_primitive_code",
]),
// declare type AllowedPropertyValues = string | number | boolean | null
properties: z
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
.optional(),
})

export type Event = z.infer<typeof eventSchema>

export function trackEvent(input: Event): void {
const event = eventSchema.parse(input)
if (event) {
va.track(event.name, event.properties)
}
}
27 changes: 26 additions & 1 deletion apps/www/lib/rehype-npm-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function rehypeNpmCommand() {
return
}

// We'll only deal with the npm install command for now.
// npm install.
if (node.properties?.["__rawString__"]?.startsWith("npm install")) {
const npmCommand = node.properties?.["__rawString__"]
node.properties["__npmCommand__"] = npmCommand
Expand All @@ -21,6 +21,31 @@ export function rehypeNpmCommand() {
"pnpm add"
)
}

// npx create.
if (node.properties?.["__rawString__"]?.startsWith("npx create-")) {
const npmCommand = node.properties?.["__rawString__"]
node.properties["__npmCommand__"] = npmCommand
node.properties["__yarnCommand__"] = npmCommand.replace(
"npx create-",
"yarn create "
)
node.properties["__pnpmCommand__"] = npmCommand.replace(
"npx create-",
"pnpm create "
)
}

// npx.
if (
node.properties?.["__rawString__"]?.startsWith("npx") &&
!node.properties?.["__rawString__"]?.startsWith("npx create-")
) {
const npmCommand = node.properties?.["__rawString__"]
node.properties["__npmCommand__"] = npmCommand
node.properties["__yarnCommand__"] = npmCommand
node.properties["__pnpmCommand__"] = npmCommand.replace("npx", "pnpx")
}
})
}
}
2 changes: 1 addition & 1 deletion apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@radix-ui/react-toggle": "^1.0.1",
"@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-tooltip": "^1.0.3",
"@vercel/analytics": "^0.1.6",
"@vercel/analytics": "^1.0.0",
"@vercel/og": "^0.0.21",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
Expand Down
8 changes: 0 additions & 8 deletions apps/www/styles/mdx.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@
@apply relative;
}

[data-rehype-pretty-code-fragment] button {
@apply opacity-0;
}

[data-rehype-pretty-code-fragment]:hover button {
@apply opacity-100;
}

[data-rehype-pretty-code-fragment] code {
@apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0;
counter-reset: line;
Expand Down
1 change: 1 addition & 0 deletions apps/www/types/unist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface UnistNode extends Node {
properties?: {
__rawString__?: string
__className__?: string
__event__?: string
[key: string]: unknown
} & NpmCommands
attributes?: {
Expand Down
Loading

0 comments on commit 5e91575

Please sign in to comment.