Skip to content
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
4 changes: 4 additions & 0 deletions apps/example/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Link from 'next/link'
import { useState } from 'react'
import { trackCopy } from '@/lib/little-friend'

type Section = 'installation' | 'upgrading' | 'quickstart' | 'core' | 'formatting' | 'relative' | 'smart' | 'compare' | 'duration' | 'timezone' | 'calendar' | 'natural' | 'react' | 'config' | 'cli'

Expand All @@ -11,6 +12,7 @@ function CodeBlock({ children, title }: { children: string; title?: string }) {

const copyToClipboard = async () => {
await navigator.clipboard.writeText(children)
trackCopy(children)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
Expand Down Expand Up @@ -246,6 +248,7 @@ function InstallationSection() {

const copyCommand = async () => {
await navigator.clipboard.writeText(addCommand)
trackCopy(addCommand)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
Expand All @@ -264,6 +267,7 @@ function InstallationSection() {
<button
onClick={async () => {
await navigator.clipboard.writeText('npx whenny init')
trackCopy('npx whenny init')
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}}
Expand Down
18 changes: 18 additions & 0 deletions apps/example/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Metadata } from 'next'
import { LITTLE_FRIEND_ENABLED, LITTLE_FRIEND_SITE_KEY } from '@/lib/little-friend'
import LittleFriendEvents from './little-friend'
import './globals.css'

const title = 'Whenny - The Modern TypeScript Date Library for the AI Era'
Expand Down Expand Up @@ -124,8 +126,24 @@ export default function RootLayout({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{LITTLE_FRIEND_ENABLED && (
<>
<script
defer
src="https://cdn.littlefriend.io/lf.js"
data-site={LITTLE_FRIEND_SITE_KEY}
data-mode="journey"
/>
<script
defer
src="https://cdn.littlefriend.io/lf-replay.js"
data-site={LITTLE_FRIEND_SITE_KEY}
/>
</>
)}
</head>
<body className="font-sans">
{LITTLE_FRIEND_ENABLED && <LittleFriendEvents />}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-slate-900 focus:text-white focus:rounded-lg focus:text-sm focus:font-medium focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
Expand Down
26 changes: 26 additions & 0 deletions apps/example/app/little-friend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { useEffect } from 'react'
import { track } from '@/lib/little-friend'

// Goals match event names, so GitHub and npm links send their own.
function onClick(event: MouseEvent) {
if (event.button > 1 || !(event.target instanceof Element)) return
const link = event.target.closest('a[href]')
if (!(link instanceof HTMLAnchorElement)) return
const host = link.hostname.replace(/^www\./, '')
if (host === 'github.com') track('github.click')
else if (host === 'npmjs.com') track('npm.click')
}

export default function LittleFriendEvents() {
useEffect(() => {
document.addEventListener('click', onClick)
document.addEventListener('auxclick', onClick)
return () => {
document.removeEventListener('click', onClick)
document.removeEventListener('auxclick', onClick)
}
}, [])
return null
}
16 changes: 10 additions & 6 deletions apps/example/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from 'next/link'
import { useState, useEffect } from 'react'
import { whenny } from 'whenny'
import { useRelativeTime } from 'whenny-react'
import { trackCopy } from '@/lib/little-friend'

// Fade-in animation wrapper
function FadeIn({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
Expand Down Expand Up @@ -56,6 +57,7 @@ function CommandBlock({ command, variant = 'dark' }: { command: string; variant?

const handleCopy = async () => {
await navigator.clipboard.writeText(command)
trackCopy(command)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
Expand Down Expand Up @@ -99,10 +101,10 @@ export default function HomePage() {
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
<Link href="/" className="font-semibold text-slate-900">Whenny</Link>
<div className="flex items-center gap-3 sm:gap-6">
<Link href="/demo" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">Demo</Link>
<Link href="/docs" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">Docs</Link>
<Link href="/demo" data-lf="nav.demo" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">Demo</Link>
<Link href="/docs" data-lf="nav.docs" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">Docs</Link>
<Link href="/blog" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">Blog</Link>
<a href="https://github.com/ZVN-DEV/whenny" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">GitHub</a>
<a href="https://github.com/ZVN-DEV/whenny" data-lf="nav.github" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900 transition-colors">GitHub</a>
</div>
</div>
</header>
Expand Down Expand Up @@ -396,10 +398,10 @@ whenny(event.date).smart()
<h2 className="text-xl sm:text-2xl font-bold text-slate-900 mb-3 sm:mb-4">Ready to simplify dates?</h2>
<p className="text-sm sm:text-base text-slate-600 mb-6 sm:mb-8">Start with the CLI or install the package. Your choice.</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link href="/docs" className="px-5 sm:px-6 py-2.5 sm:py-3 bg-slate-900 text-white rounded-lg font-medium hover:bg-slate-800 transition-colors text-sm sm:text-base">
<Link href="/docs" data-lf="cta.docs" className="px-5 sm:px-6 py-2.5 sm:py-3 bg-slate-900 text-white rounded-lg font-medium hover:bg-slate-800 transition-colors text-sm sm:text-base">
Read the Docs
</Link>
<Link href="/demo" className="px-5 sm:px-6 py-2.5 sm:py-3 bg-white text-slate-900 border border-slate-200 rounded-lg font-medium hover:bg-slate-50 transition-colors text-sm sm:text-base">
<Link href="/demo" data-lf="cta.demo" className="px-5 sm:px-6 py-2.5 sm:py-3 bg-white text-slate-900 border border-slate-200 rounded-lg font-medium hover:bg-slate-50 transition-colors text-sm sm:text-base">
Try the Demo
</Link>
</div>
Expand All @@ -414,7 +416,7 @@ whenny(event.date).smart()
<div className="flex items-center gap-4 sm:gap-6">
<Link href="/docs" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900">Docs</Link>
<Link href="/demo" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900">Demo</Link>
<a href="https://github.com/ZVN-DEV/whenny" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900">GitHub</a>
<a href="https://github.com/ZVN-DEV/whenny" data-lf="footer.github" className="text-xs sm:text-sm text-slate-600 hover:text-slate-900">GitHub</a>
</div>
</div>
</footer>
Expand Down Expand Up @@ -471,6 +473,7 @@ function ModuleChip({ name }: { name: string }) {

const handleCopy = async () => {
await navigator.clipboard.writeText(command)
trackCopy(command)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
Expand All @@ -497,6 +500,7 @@ function CopyableLine({ command }: { command: string }) {

const handleCopy = async () => {
await navigator.clipboard.writeText(command)
trackCopy(command)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
Expand Down
34 changes: 34 additions & 0 deletions apps/example/lib/little-friend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Little Friend analytics (littlefriend.io). The site key is public: it only
// names the project the tracker reports to. The tracker sets no cookies, reads
// no form values or page text, and strips query strings.

export const LITTLE_FRIEND_SITE_KEY = 'lf_om3kqCX76KvI2c9G0Yt5UMoc'

// Production deployments only, so previews and local dev send nothing.
export const LITTLE_FRIEND_ENABLED = process.env.VERCEL_ENV === 'production'

type LittleFriendProps = Record<string, string | number | boolean>

declare global {
interface Window {
lf?: (command: string, ...args: unknown[]) => void
}
}

// A named event. Does nothing until the tracker has loaded, and nothing at
// all outside production, where the tag is not rendered.
export function track(name: string, props?: LittleFriendProps) {
if (typeof window === 'undefined') return
window.lf?.('track', name, props)
}

// After a copy: install commands send install.copy with how they install.
// The command text itself is never sent.
export function trackCopy(text: string) {
const command = text.trim()
if (command.startsWith('npx create-whenny')) track('install.copy', { method: 'create' })
else if (command.startsWith('npx whenny')) track('install.copy', { method: 'cli' })
else if (/^(npm install|npm i|pnpm add|yarn add|bun add) /.test(command)) {
track('install.copy', { method: 'package' })
}
}
2 changes: 1 addition & 1 deletion apps/example/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const nextConfig = {
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none';",
value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.littlefriend.io; connect-src 'self' https://in.littlefriend.io; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none';",
},
{
key: 'Strict-Transport-Security',
Expand Down
Loading