diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..726a201 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.11.1 \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f3489a9 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "plugins": [ + "prettier-plugin-organize-imports", + "prettier-plugin-tailwindcss" + ], + "tailwindConfig": "./tailwind.config.js", + "tailwindFunctions": ["tv"] +} diff --git a/app/layout.tsx b/app/layout.tsx index 43c910f..48358f2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,24 +1,8 @@ import 'tailwindcss/tailwind.css' -import { IBM_Plex_Mono, Inter, PT_Serif } from 'next/font/google' +import { karelia } from '@/fonts/fonts' -const serif = PT_Serif({ - variable: '--font-serif', - style: ['normal', 'italic'], - subsets: ['latin'], - weight: ['400', '700'], -}) -const sans = Inter({ - variable: '--font-sans', - subsets: ['latin'], - // @todo: understand why extrabold (800) isn't being respected when explicitly specified in this weight array - // weight: ['500', '700', '800'], -}) -const mono = IBM_Plex_Mono({ - variable: '--font-mono', - subsets: ['latin'], - weight: ['500', '700'], -}) +import { Providers } from './providers' export default async function RootLayout({ children, @@ -26,11 +10,10 @@ export default async function RootLayout({ children: React.ReactNode }) { return ( - - {children} + + + {children} + ) } diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..096776c --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next/types' + +import { HomePage } from '@/components/pages/home/HomePage' + +export const metadata: Metadata = { + title: 'Datum', +} + +export default async function Homepage() { + return +} diff --git a/app/providers.tsx b/app/providers.tsx new file mode 100644 index 0000000..1012cc7 --- /dev/null +++ b/app/providers.tsx @@ -0,0 +1,15 @@ +'use client' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const queryClient = new QueryClient() + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + + ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..d2b9162 --- /dev/null +++ b/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "styles/index.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/global/Logo.tsx b/components/global/Logo.tsx new file mode 100644 index 0000000..c03515a --- /dev/null +++ b/components/global/Logo.tsx @@ -0,0 +1,25 @@ +type LogoProps = { + w?: number + h?: number +} + +export const Logo = ({ w = 145, h = 39 }: LogoProps) => { + return ( + + + + + ) +} diff --git a/components/pages/home/HomePage.animation.tsx b/components/pages/home/HomePage.animation.tsx new file mode 100644 index 0000000..2e6503a --- /dev/null +++ b/components/pages/home/HomePage.animation.tsx @@ -0,0 +1,161 @@ +'use client' +import Matter from 'matter-js' +import React, { useEffect, useRef } from 'react' + +export const HomePageAnimation = () => { + const sceneRef = useRef(null) + + useEffect(() => { + const { + Engine, + Render, + Runner, + Bodies, + Composite, + Composites, + Constraint, + Mouse, + MouseConstraint, + Common, + Events, + Body, + } = Matter + + const currentRef = sceneRef.current + if (!currentRef) return + + const engine = Engine.create() + const world = engine.world + const render = Render.create({ + element: currentRef, + engine: engine, + options: { + width: 1000, + height: 1100, + wireframes: true, + background: 'white', + }, + }) + + Render.run(render) + const runner = Runner.create() + Runner.run(runner, engine) + + // create obstacles + const obstacles = Composites.stack(10, 0, 15, 3, 10, 10, function (x, y) { + let sides = Math.round(Common.random(1, 8)), + options = { + render: { + fillStyle: Common.choose([ + '#48f164', + '#f5d259', + '#f55a3c', + '#063e7b', + '#ececd1', + ]), + }, + } + + switch (Math.round(Common.random(0, 1))) { + case 0: + if (Common.random() < 0.8) { + return Bodies.rectangle( + x, + y, + Common.random(25, 50), + Common.random(25, 50), + options, + ) + } else { + return Bodies.rectangle( + x, + y, + Common.random(80, 120), + Common.random(25, 30), + options, + ) + } + case 1: + return Bodies.polygon(x, y, sides, Common.random(25, 50), options) + } + }) + + Composite.add(world, [obstacles]) + + let timeScaleTarget = 1, + lastTime = Common.now() + + Events.on(engine, 'afterUpdate', function (event) { + var timeScale = (event.delta || 1000 / 60) / 1000 + + // tween the timescale for slow-mo + if (mouse.button === -1) { + engine.timing.timeScale += + (timeScaleTarget - engine.timing.timeScale) * 3 * timeScale + } else { + engine.timing.timeScale = 1 + } + + // every 2 sec (real time) + if (Common.now() - lastTime >= 2000) { + // flip the timescale + if (timeScaleTarget < 1) { + timeScaleTarget = 1 + } else { + timeScaleTarget = 0.05 + } + + // update last time + lastTime = Common.now() + } + + for (let i = 0; i < obstacles.bodies.length; i += 1) { + var body = obstacles.bodies[i], + bounds = body.bounds + + // move obstacles back to the top of the screen + if (bounds.min.y > render.bounds.max.y + 100) { + Body.translate(body, { + x: -bounds.min.x, + y: -render.bounds.max.y - 300, + }) + } + } + }) + + // add mouse control and make the mouse revolute + var mouse = Mouse.create(render.canvas), + mouseConstraint = MouseConstraint.create(engine, { + mouse: mouse, + constraint: { + stiffness: 0.6, + length: 0, + angularStiffness: 0, + render: { + visible: false, + }, + }, + }) + + Composite.add(world, mouseConstraint) + + // keep the mouse in sync with rendering + render.mouse = mouse + + // fit the render viewport to the scene + Render.lookAt(render, { + min: { x: 0, y: 0 }, + max: { x: 800, y: 600 }, + }) + + return () => { + Render.stop(render) + Runner.stop(runner) + while (currentRef.firstChild) { + currentRef.removeChild(currentRef.firstChild) + } + } + }, []) + + return
+} diff --git a/components/pages/home/HomePage.newsletter.tsx b/components/pages/home/HomePage.newsletter.tsx new file mode 100644 index 0000000..5a0947b --- /dev/null +++ b/components/pages/home/HomePage.newsletter.tsx @@ -0,0 +1,86 @@ +'use client' + +import { zodResolver } from '@hookform/resolvers/zod' +import { LoaderCircle, MailCheck } from 'lucide-react' +import { useForm } from 'react-hook-form' +import { z } from 'zod' + +import { + Button, + Form, + FormControl, + FormField, + FormMessage, + Input, +} from '@/components/ui' +import { useSubscribeMutation } from '@/hooks/mutations/useSubscribeMutation' + +import { newsletterStyles } from './HomePage.styles' + +const formSchema = z.object({ + email: z.string().email(), +}) + +export const HomePageNewsletter = () => { + const { + wrapper, + input, + button, + errorMessage, + success, + successMessage, + successIcon, + } = newsletterStyles() + const { mutate, status, isError, error, data } = useSubscribeMutation() + const isLoading = status === 'pending' + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + }, + }) + + const onSubmit = ({ email }: z.infer) => { + mutate(email) + } + + return ( + <> + {status === 'success' ? ( +
+ + {data.message} +
+ ) : ( +
+ + ( + <> + + + + + + )} + /> + + + {isError &&
{error.message}
} + + )} + + ) +} diff --git a/components/pages/home/HomePage.styles.ts b/components/pages/home/HomePage.styles.ts new file mode 100644 index 0000000..03d970e --- /dev/null +++ b/components/pages/home/HomePage.styles.ts @@ -0,0 +1,34 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +const homeStyles = tv({ + slots: { + base: 'font-mono text-white h-screen overflow-hidden flex flex-col-reverse md:flex-row', + left: 'w-full bg-blackberry-800 flex justify-center py-10 md:py-16 md:w-1/2', + right: 'w-full bg-white flex flex-1 relative md:w-1/2', + leftInner: + 'relative gap-9 md:gap-0 md:max-w-xl flex flex-col justify-between px-6', + rightImage: 'object-cover', + heading: 'text-2xl leading-snug md:text-4xl', + footer: 'hidden md:block', + githubMobile: 'z-auto block absolute top-0 right-8 md:hidden', + }, +}) + +const newsletterStyles = tv({ + slots: { + wrapper: 'relative mt-14 flex flex-col gap-5 md:flex-row', + input: 'w-full h-12 md:h-auto', + button: + 'absolute top-2 h-8 right-2 text-xs md:relative md:text-sm md:top-0 md:h-14', + errorMessage: 'text-sunglow-900 mt-3', + success: + 'mt-14 bg-white bg-opacity-20 p-5 rounded-md text-white flex gap-3 ', + successMessage: 'flex-1', + successIcon: 'mt-1', + }, +}) + +export type HomeVariants = VariantProps +export type NewsletterVariants = VariantProps + +export { homeStyles, newsletterStyles } diff --git a/components/pages/home/HomePage.tsx b/components/pages/home/HomePage.tsx index b8e70ba..c307c30 100644 --- a/components/pages/home/HomePage.tsx +++ b/components/pages/home/HomePage.tsx @@ -1,52 +1,74 @@ -import { ProjectListItem } from 'components/pages/home/ProjectListItem' -import { Header } from 'components/shared/Header' -import Layout from 'components/shared/Layout' -import ScrollUp from 'components/shared/ScrollUp' -import { resolveHref } from 'lib/sanity.links' +import Image from 'next/image' import Link from 'next/link' -import type { HomePagePayload } from 'types' -import { SettingsPayload } from 'types' -import HomePageHead from './HomePageHead' +import { Logo } from '@/components/global/Logo' +import { Button } from '@/components/ui' +import { GITHUB_URL } from '@/constants' -export interface HomePageProps { - settings?: SettingsPayload - page?: HomePagePayload - preview?: boolean -} +import { HomePageNewsletter } from './HomePage.newsletter' +import { homeStyles } from './HomePage.styles' -export function HomePage({ page, settings, preview }: HomePageProps) { - const { overview, showcaseProjects, title = 'Personal website' } = page ?? {} +export function HomePage() { + const { + base, + left, + right, + rightImage, + leftInner, + heading, + footer, + githubMobile, + } = homeStyles() return ( - <> - - - -
- {/* Header */} - {title &&
} - {/* Showcase projects */} - {showcaseProjects && showcaseProjects.length > 0 && ( -
- {showcaseProjects.map((project, key) => { - const href = resolveHref(project._type, project.slug) - if (!href) { - return null - } - return ( - - - - ) - })} -
- )} - - {/* Workaround: scroll to top on route change */} - +
+
+
+
+ +
+
+

+ Every foundational tool that software companies need for + hyper-scale, backed by open source. +

+ +
+
+ +
+
+ +
- - +
+
+ +
+
) } diff --git a/components/pages/home/HomePageHead.tsx b/components/pages/home/HomePageHead.tsx deleted file mode 100644 index 3a18a5d..0000000 --- a/components/pages/home/HomePageHead.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { toPlainText } from '@portabletext/react' -import { SiteMeta } from 'components/global/SiteMeta' -import { HomePagePayload, SettingsPayload } from 'types' - -export interface HomePageHeadProps { - settings?: SettingsPayload - page?: HomePagePayload -} - -export default function HomePageHead({ settings, page }: HomePageHeadProps) { - return ( - - ) -} diff --git a/components/pages/home/HomePagePreview.tsx b/components/pages/home/HomePagePreview.tsx deleted file mode 100644 index 277ea55..0000000 --- a/components/pages/home/HomePagePreview.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { homePageQuery } from 'lib/sanity.queries' -import { useLiveQuery } from 'next-sanity/preview' -import type { HomePagePayload } from 'types' - -import { HomePage, HomePageProps } from './HomePage' - -export default function HomePagePreview({ - page: initialPage, - settings, -}: HomePageProps) { - const [page] = useLiveQuery( - initialPage, - homePageQuery, - ) - - if (!page) { - return ( -
- Please start editing your Home document to see the preview! -
- ) - } - - return -} diff --git a/components/pages/home/ProjectListItem.tsx b/components/pages/home/ProjectListItem.tsx deleted file mode 100644 index 98fcade..0000000 --- a/components/pages/home/ProjectListItem.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { CustomPortableText } from 'components/shared/CustomPortableText' -import ImageBox from 'components/shared/ImageBox' -import type { ShowcaseProject } from 'types' - -interface ProjectProps { - project: ShowcaseProject - odd: number -} - -export function ProjectListItem(props: ProjectProps) { - const { project, odd } = props - - return ( -
-
- -
-
- -
-
- ) -} - -function TextBox({ project }: { project: ShowcaseProject }) { - return ( -
-
- {/* Title */} -
- {project.title} -
- {/* Overview */} -
- -
-
- {/* Tags */} -
- {project.tags?.map((tag, key) => ( -
- #{tag} -
- ))} -
-
- ) -} diff --git a/components/ui/Button/Button.styles.tsx b/components/ui/Button/Button.styles.tsx new file mode 100644 index 0000000..8298ee4 --- /dev/null +++ b/components/ui/Button/Button.styles.tsx @@ -0,0 +1,38 @@ +import { tv, type VariantProps } from 'tailwind-variants' + +export const button = tv({ + base: 'font-mono inline-flex items-center gap-2 justify-center whitespace-nowrap rounded-md text-sm ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300', + variants: { + variant: { + default: + 'bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90', + destructive: + 'bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90', + outline: + 'border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50', + secondary: 'bg-sunglow-900', + ghost: + 'hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50', + link: 'text-slate-900 underline-offset-4 hover:underline dark:text-slate-50', + white: 'bg-white text-blackberry-800', + }, + size: { + default: 'h-14 px-7 text-lg', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10 px-0 py-0', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, +}) + +type ButtonVariants = VariantProps + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + ButtonVariants { + asChild?: boolean +} diff --git a/components/ui/Button/Button.tsx b/components/ui/Button/Button.tsx new file mode 100644 index 0000000..7dd393f --- /dev/null +++ b/components/ui/Button/Button.tsx @@ -0,0 +1,21 @@ +import { Slot } from '@radix-ui/react-slot' +import { forwardRef } from 'react' + +import { button, type ButtonProps } from './Button.styles' + +const Button = forwardRef( + ({ asChild = false, className, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + }, +) + +Button.displayName = 'Button' + +export { Button, button } diff --git a/components/ui/Form/Form.tsx b/components/ui/Form/Form.tsx new file mode 100644 index 0000000..6e9a1a0 --- /dev/null +++ b/components/ui/Form/Form.tsx @@ -0,0 +1,181 @@ +'use client' + +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { createContext, forwardRef, useContext, useId } from 'react' +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form' + +import { Label } from '@/components/ui/Label/Label' +import { cn } from '@/lib/utils' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = createContext( + {} as FormFieldContextValue, +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = useContext(FormFieldContext) + const itemContext = useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = createContext( + {} as FormItemContextValue, +) + +const FormItem = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +