Skip to content

Commit

Permalink
first draft of pitchdeck
Browse files Browse the repository at this point in the history
  • Loading branch information
emilwidlund committed Jan 16, 2025
1 parent 7ddd23b commit fa8908b
Show file tree
Hide file tree
Showing 18 changed files with 780 additions and 0 deletions.
Binary file added clients/apps/web/public/assets/team/birk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added clients/apps/web/public/assets/team/emil.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added clients/apps/web/public/assets/team/francois.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions clients/apps/web/src/app/pitch/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function PitchLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="bg-polar-900 text-polar-100 flex h-screen w-screen flex-col overflow-auto p-12 font-mono text-sm">
{children}
</div>
)
}
64 changes: 64 additions & 0 deletions clients/apps/web/src/app/pitch/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client'

import { Footer } from '@/components/Pitch/Footer'
import { IndexSection } from '@/components/Pitch/sections/IndexSection'
import { InvestorsSection } from '@/components/Pitch/sections/InvestorsSection'
import { Polar20Section } from '@/components/Pitch/sections/Polar20'
import { TeamSection } from '@/components/Pitch/sections/TeamSection'
import { UsageBasedSection } from '@/components/Pitch/sections/UsageBasedSection'
import { WhySection } from '@/components/Pitch/sections/WhySection'
import { useArrowFocus } from '@/components/Pitch/useArrowFocus'
import { AnimatePresence, motion } from 'framer-motion'
import { useCallback, useState } from 'react'
import { PitchNavigation, sections } from '../../components/Pitch/Navigation'

export default function PitchPage() {
const [index, setIndex] = useState(0)

useArrowFocus({
onLeft: () => setIndex((index) => Math.max(0, index - 1)),
onRight: () =>
setIndex((index) => Math.min(sections.length - 1, index + 1)),
onNumberPress: (number) =>
setIndex(Math.max(0, Math.min(number, sections.length - 1))),
})

const getActiveSection = useCallback(() => {
switch (index) {
case 0:
default:
return <IndexSection />
case 1:
return <UsageBasedSection />
case 2:
return <WhySection />
case 3:
return <Polar20Section />
case 4:
return <IndexSection />
case 5:
return <TeamSection />
case 6:
return <InvestorsSection />
}
}, [index])

return (
<div className="flex h-full flex-col justify-between gap-y-12 text-sm">
<div className="flex flex-grow flex-col gap-y-32">
<PitchNavigation activeIndex={index} />
<AnimatePresence key={index}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.04, repeat: 2 }}
>
{getActiveSection()}
</motion.div>
</AnimatePresence>
</div>
<Footer />
</div>
)
}
28 changes: 28 additions & 0 deletions clients/apps/web/src/components/Pitch/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PropsWithChildren } from 'react'
import { twMerge } from 'tailwind-merge'

export interface ButtonProps {
className?: string
variant?: 'primary' | 'icon'
}

export const Button = ({
children,
className,
variant = 'primary',
}: PropsWithChildren<ButtonProps>) => {
const primaryClassName = 'p-2 text-xs'
const iconClassName = 'h-4 w-4 text-xxs'

return (
<button
className={twMerge(
'border-polar-200 hover:bg-polar-200 flex flex-col items-center justify-center border-[0.5px] border-b-2 font-mono leading-none focus-within:bg-white focus-within:text-black focus-within:outline-none hover:text-black',
variant === 'primary' ? primaryClassName : iconClassName,
className,
)}
>
{children}
</button>
)
}
225 changes: 225 additions & 0 deletions clients/apps/web/src/components/Pitch/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { getValueFormatter } from '@/utils/metrics'
import * as Plot from '@observablehq/plot'
import { Interval, Metric } from '@polar-sh/sdk'
import * as d3 from 'd3'
import { GeistMono } from 'geist/font/mono'
import { useCallback, useEffect, useMemo, useState } from 'react'

const primaryColor = 'rgb(0 98 255)'
const primaryColorFaded = 'rgba(0, 98, 255, 0.3)'
const gradientId = 'chart-gradient'
const createAreaGradient = (id: string) => {
// Create a <defs> element
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')

// Create a <linearGradient> element
const linearGradient = document.createElementNS(
'http://www.w3.org/2000/svg',
'linearGradient',
)
linearGradient.setAttribute('id', id)
linearGradient.setAttribute('gradientTransform', 'rotate(90)')

// Create the first <stop> element
const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
stop1.setAttribute('offset', '0%')
stop1.setAttribute('stop-color', primaryColorFaded)
stop1.setAttribute('stop-opacity', '0.5')

// Create the second <stop> element
const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop')
stop2.setAttribute('offset', '100%')
stop2.setAttribute('stop-color', primaryColorFaded)
stop2.setAttribute('stop-opacity', '0')

// Append the <stop> elements to the <linearGradient> element
linearGradient.appendChild(stop1)
linearGradient.appendChild(stop2)

// Append the <linearGradient> element to the <defs> element
defs.appendChild(linearGradient)

return defs
}

class Callback extends Plot.Dot {
private callbackFunction: (index: number | undefined) => void

public constructor(
data: Plot.Data,
options: Plot.DotOptions,
callbackFunction: (data: any) => void,
) {
// @ts-ignore
super(data, options)
this.callbackFunction = callbackFunction
}

// @ts-ignore
public render(
index: number[],
_scales: Plot.ScaleFunctions,
_values: Plot.ChannelValues,
_dimensions: Plot.Dimensions,
_context: Plot.Context,
_next?: Plot.RenderFunction,
): SVGElement | null {
if (index.length) {
this.callbackFunction(index[0])
}
return null
}
}

const getTicks = (timestamps: Date[], maxTicks: number = 10): Date[] => {
const step = Math.ceil(timestamps.length / maxTicks)
return timestamps.filter((_, index) => index % step === 0)
}

const getTickFormat = (
interval: Interval,
ticks: Date[],
): ((t: Date, i: number) => any) | string => {
switch (interval) {
case Interval.HOUR:
return (t: Date, i: number) => {
const previousDate = ticks[i - 1]
if (!previousDate || previousDate.getDate() < t.getDate()) {
return d3.timeFormat('%a %H:%M')(t)
}
return d3.timeFormat('%H:%M')(t)
}
case Interval.DAY:
return '%b %d'
case Interval.WEEK:
return '%b %d'
case Interval.MONTH:
return '%b %y'
case Interval.YEAR:
return '%Y'
}
}

interface ChartProps {
data: {
timestamp: Date
value: number
}[]
interval: Interval
metric: Metric
height?: number
maxTicks?: number
onDataIndexHover?: (index: number | undefined) => void
}

export const Chart: React.FC<ChartProps> = ({
data,
interval,
metric,
height: _height,
maxTicks: _maxTicks,
onDataIndexHover,
}) => {
const [width, setWidth] = useState(0)
const height = useMemo(() => _height || 400, [_height])
const maxTicks = useMemo(() => _maxTicks || 10, [_maxTicks])

const timestamps = useMemo(
() => data.map(({ timestamp }) => timestamp),
[data],
)
const ticks = useMemo(
() => getTicks(timestamps, maxTicks),
[timestamps, maxTicks],
)
const valueFormatter = useMemo(() => getValueFormatter(metric), [metric])

const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null)

useEffect(() => {
const resizeObserver = new ResizeObserver((_entries) => {
if (containerRef) {
setWidth(containerRef.clientWidth ?? 0)
}
})

if (containerRef) {
resizeObserver.observe(containerRef)
}

return () => {
if (containerRef) {
resizeObserver.unobserve(containerRef)
}
}
}, [containerRef])

const onMouseLeave = useCallback(() => {
if (onDataIndexHover) {
onDataIndexHover(undefined)
}
}, [onDataIndexHover])

useEffect(() => {
if (!containerRef) {
return
}

const plot = Plot.plot({
style: {
background: 'none',
},
width,
height,
marks: [
() => createAreaGradient(gradientId),
Plot.axisX({
tickFormat: getTickFormat(interval, ticks),
ticks,
label: null,
stroke: 'none',
fontFamily: GeistMono.style.fontFamily,
}),
Plot.axisY({
tickFormat: valueFormatter,
label: null,
stroke: 'none',
fontFamily: GeistMono.style.fontFamily,
}),
Plot.lineY(data, {
x: 'timestamp',
y: metric.slug,
stroke: 'currentColor',
strokeWidth: 1,
}),
Plot.ruleX(data, {
x: 'timestamp',
stroke: 'currentColor',
strokeWidth: 1,
strokeOpacity: 0.1,
}),
],
})
containerRef.append(plot)

return () => plot.remove()
}, [
data,
metric,
containerRef,
interval,
ticks,
valueFormatter,
width,
height,
onDataIndexHover,
])

return (
<div
className="dark:text-polar-500 w-full"
ref={setContainerRef}
onMouseLeave={onMouseLeave}
/>
)
}
25 changes: 25 additions & 0 deletions clients/apps/web/src/components/Pitch/Console.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { twMerge } from 'tailwind-merge'

export interface ConsoleProps {
className?: string
input?: string
output?: string
}

export const Console = ({ className, input, output }: ConsoleProps) => {
return (
<div
className={twMerge('border-polar-200 flex flex-col border', className)}
>
<div className="bg-polar-200 flex flex-row px-3 py-1 text-xs text-black">
<span>Polar VM</span>
</div>
<div className="flex flex-col p-4 font-mono text-sm">
<pre className="flex flex-col gap-y-2">
<code>{'$ ' + input}</code>
<code className="text-polar-500">{output}</code>
</pre>
</div>
</div>
)
}
Loading

0 comments on commit fa8908b

Please sign in to comment.