-
-
Notifications
You must be signed in to change notification settings - Fork 175
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7ddd23b
commit fa8908b
Showing
18 changed files
with
780 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
Oops, something went wrong.