├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── eslint.config.js ├── instrumentation.ts ├── next-sitemap.config.js ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── basis-transcoder │ ├── basis_transcoder.js │ ├── basis_transcoder.js.backup │ └── basis_transcoder.wasm ├── fonts │ ├── ffflauta.ts │ └── flauta.ttf ├── images │ ├── logobasement.png │ └── tile.png └── readme │ └── hero.gif ├── src ├── actions │ ├── contact-form │ │ ├── index.ts │ │ └── template.ts │ └── laboratory-fetch │ │ └── index.ts ├── app │ ├── (pages) │ │ ├── (home) │ │ │ ├── basehub.ts │ │ │ ├── brands-mobile.tsx │ │ │ ├── brands.tsx │ │ │ ├── capabilities.tsx │ │ │ ├── featured-projects.tsx │ │ │ ├── intro.tsx │ │ │ ├── page.tsx │ │ │ ├── query.ts │ │ │ └── showcase-image.tsx │ │ ├── blog │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ ├── basehub.ts │ │ │ ├── featured.tsx │ │ │ ├── hero.tsx │ │ │ └── layout.tsx │ │ ├── layout.tsx │ │ ├── people │ │ │ ├── careers-query.ts │ │ │ ├── crew.tsx │ │ │ ├── hero.tsx │ │ │ ├── open-positions.tsx │ │ │ ├── page.tsx │ │ │ ├── pre-open-positions.tsx │ │ │ ├── query.ts │ │ │ └── values.tsx │ │ ├── post │ │ │ └── [slug] │ │ │ │ ├── back.tsx │ │ │ │ ├── blog-components.tsx │ │ │ │ ├── blog-meta.tsx │ │ │ │ ├── components │ │ │ │ ├── code-block-header.tsx │ │ │ │ ├── code-block.module.css │ │ │ │ ├── code-block.tsx │ │ │ │ ├── code-icons.tsx │ │ │ │ ├── rauchg.jpg │ │ │ │ ├── sandbox │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── list │ │ │ │ │ │ ├── daylight-sandbox.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── sandbox.module.css │ │ │ │ │ ├── sandpack-styles.tsx │ │ │ │ │ ├── templates │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── p5.ts │ │ │ │ │ ├── theme.ts │ │ │ │ │ └── toolbar.tsx │ │ │ │ └── tweet.tsx │ │ │ │ ├── content.tsx │ │ │ │ ├── more.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── query.ts │ │ │ │ └── title.tsx │ │ ├── services │ │ │ ├── awards.tsx │ │ │ ├── hero.tsx │ │ │ ├── page.tsx │ │ │ ├── query.ts │ │ │ ├── services.tsx │ │ │ ├── testimonials.module.css │ │ │ ├── testimonials.tsx │ │ │ └── ventures.tsx │ │ └── showcase │ │ │ ├── [slug] │ │ │ ├── back.tsx │ │ │ ├── basehub.ts │ │ │ ├── context.tsx │ │ │ ├── gallery-filter.tsx │ │ │ ├── gallery.tsx │ │ │ ├── info.tsx │ │ │ ├── page.tsx │ │ │ ├── people.tsx │ │ │ ├── query.ts │ │ │ ├── related.tsx │ │ │ └── wrapper.tsx │ │ │ ├── basehub.ts │ │ │ ├── filters.tsx │ │ │ ├── grid.tsx │ │ │ ├── hero.tsx │ │ │ ├── list.tsx │ │ │ ├── page.tsx │ │ │ ├── query.ts │ │ │ └── showcase-list │ │ │ ├── client.tsx │ │ │ └── index.tsx │ ├── actions │ │ └── subscribe.ts │ ├── api │ │ └── scores │ │ │ └── route.ts │ ├── basketball │ │ ├── client.tsx │ │ └── page.tsx │ ├── contact │ │ ├── contact-footer.tsx │ │ ├── form │ │ │ ├── contact-form.tsx │ │ │ ├── contact-input.tsx │ │ │ └── contact-status.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── global-error.tsx │ ├── lab │ │ ├── client.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── not-found.tsx ├── components │ ├── app-hooks-init │ │ └── index.tsx │ ├── arcade-board │ │ ├── button.tsx │ │ ├── check-sequence.ts │ │ ├── coffee-steam.tsx │ │ ├── constants.ts │ │ ├── index.tsx │ │ └── stick.tsx │ ├── arcade-game │ │ ├── entities │ │ │ ├── car │ │ │ │ └── index.tsx │ │ │ └── grid.tsx │ │ ├── hooks │ │ │ ├── use-interval.tsx │ │ │ ├── use-state-to-ref.ts │ │ │ ├── use-timeout.ts │ │ │ └── use-uniforms.ts │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── colors.ts │ │ │ ├── connector.ts │ │ │ ├── math.ts │ │ │ ├── subscribable.ts │ │ │ ├── uniforms.ts │ │ │ ├── use-controls.ts │ │ │ └── use-game.ts │ │ ├── npc │ │ │ ├── index.tsx │ │ │ ├── npc-types │ │ │ │ └── motorcycle.tsx │ │ │ └── use-npc.tsx │ │ ├── player │ │ │ ├── death-animation.tsx │ │ │ └── index.tsx │ │ ├── road │ │ │ ├── chunks │ │ │ │ └── base-road.tsx │ │ │ ├── get-chunk.tsx │ │ │ ├── index.tsx │ │ │ └── use-road.ts │ │ ├── sensors │ │ │ └── index.tsx │ │ └── skybox │ │ │ └── index.tsx │ ├── arcade-screen │ │ ├── arcade-ui-components │ │ │ ├── arcade-featured.tsx │ │ │ ├── arcade-labs-list.tsx │ │ │ ├── arcade-preview.tsx │ │ │ ├── arcade-title-tags-header.tsx │ │ │ └── arcade-wrapper-tags.tsx │ │ ├── index.tsx │ │ ├── query.ts │ │ ├── render-texture.tsx │ │ └── screen-ui.tsx │ ├── assets-provider │ │ ├── fetch-assets.ts │ │ ├── fragments.ts │ │ ├── index.tsx │ │ └── query.ts │ ├── basketball │ │ ├── arcade-name-input.tsx │ │ ├── basketball-utils.ts │ │ ├── basketball.tsx │ │ ├── error-boundary.tsx │ │ ├── hoop-minigame.tsx │ │ ├── letter-slot.tsx │ │ ├── net.tsx │ │ ├── rigid-bodies.tsx │ │ └── scoreboard.tsx │ ├── blog-door │ │ ├── constants.ts │ │ └── index.tsx │ ├── blog │ │ ├── categories │ │ │ ├── client.tsx │ │ │ └── index.tsx │ │ └── list │ │ │ └── index.tsx │ ├── brands │ │ └── index.tsx │ ├── camera │ │ ├── camera-controller.tsx │ │ ├── camera-controls.tsx │ │ ├── camera-hooks.tsx │ │ ├── camera-utils.ts │ │ └── wasd-controls.tsx │ ├── characters │ │ ├── character-instancer.tsx │ │ ├── character-utils.ts │ │ ├── characters-config.ts │ │ ├── characters-spawn.tsx │ │ ├── index.tsx │ │ └── instanced-skinned-mesh │ │ │ ├── index.tsx │ │ │ ├── instanced-skinned-mesh.ts │ │ │ └── notes │ │ │ ├── animation-interpolation-notes.md │ │ │ └── morph-notes.md │ ├── clock │ │ └── index.tsx │ ├── contact │ │ ├── contact-canvas.tsx │ │ ├── contact-scene.tsx │ │ ├── contact-screen.tsx │ │ ├── contact-store.ts │ │ ├── contact.interface.ts │ │ ├── contact.tsx │ │ └── fallback.tsx │ ├── custom-cursor │ │ └── index.tsx │ ├── debug │ │ ├── index.tsx │ │ ├── only-debug.tsx │ │ └── react-scan.tsx │ ├── godrays │ │ └── index.tsx │ ├── icons │ │ └── icons.tsx │ ├── inspectables │ │ ├── context.tsx │ │ ├── inspectable-dragger.tsx │ │ ├── inspectable-viewer.tsx │ │ ├── inspectable.tsx │ │ ├── inspectables.tsx │ │ └── use-fade-animation.ts │ ├── lamp │ │ └── index.tsx │ ├── layout │ │ ├── contact.tsx │ │ ├── content-wrapper.tsx │ │ ├── footer-content.tsx │ │ ├── footer.tsx │ │ ├── music-toggle.tsx │ │ ├── navbar-content.tsx │ │ ├── navbar.tsx │ │ ├── query.tsx │ │ ├── shared-sections.tsx │ │ └── stay-connected.tsx │ ├── loading │ │ ├── app-loading-handler.tsx │ │ ├── fallback-loading.tsx │ │ ├── loading-canvas.tsx │ │ └── loading-scene │ │ │ └── index.tsx │ ├── locked-door │ │ └── index.tsx │ ├── map │ │ ├── bakes.tsx │ │ ├── extract-meshes.ts │ │ ├── index.tsx │ │ ├── use-frame-loop.ts │ │ └── use-loader.ts │ ├── navigation-handler │ │ ├── index.tsx │ │ ├── navigation-store.ts │ │ └── navigation.interface.ts │ ├── outdoor-cars │ │ └── index.tsx │ ├── pets │ │ └── index.tsx │ ├── posthog │ │ └── posthog-provider.tsx │ ├── postprocessing │ │ ├── post-processing.tsx │ │ ├── renderer.tsx │ │ └── use-postprocessing-settings.tsx │ ├── primitives │ │ ├── icons │ │ │ └── arrow.tsx │ │ ├── image-with-video-overlay.tsx │ │ ├── info-item.tsx │ │ ├── input.tsx │ │ ├── link.tsx │ │ ├── placeholder.tsx │ │ ├── portal.tsx │ │ ├── rich-text.tsx │ │ ├── scroll-down.tsx │ │ ├── text-list.tsx │ │ └── video.tsx │ ├── routing-element │ │ ├── frag.glsl │ │ ├── routing-arrow.tsx │ │ ├── routing-element.tsx │ │ ├── routing-plus.tsx │ │ └── vert.glsl │ ├── scene │ │ └── index.tsx │ ├── shared │ │ └── AnimationController.tsx │ ├── sparkles │ │ ├── frag.glsl │ │ ├── index.tsx │ │ └── vert.glsl │ ├── speaker-hover │ │ └── index.tsx │ ├── transitions │ │ ├── index.tsx │ │ └── transitions.css │ ├── tunnel │ │ └── index.tsx │ └── weather │ │ └── index.tsx ├── constants │ ├── inspectables.ts │ ├── sparkles.ts │ └── transitions.ts ├── declarations │ ├── glsl.d.ts │ └── shader.d.ts ├── hooks │ ├── use-ambience-playlist.ts │ ├── use-audio-urls.ts │ ├── use-console-logo.ts │ ├── use-current-scene.ts │ ├── use-debounce-value.ts │ ├── use-device-detect.ts │ ├── use-disable-scroll.ts │ ├── use-focus-trap.ts │ ├── use-handle-contact.ts │ ├── use-handle-navigation.ts │ ├── use-is-on-tab.ts │ ├── use-key-press.ts │ ├── use-ktx2-gltf.ts │ ├── use-media.ts │ ├── use-mesh.ts │ ├── use-mouse-pos.ts │ ├── use-mouse.ts │ ├── use-pausable-time.ts │ ├── use-preload-assets.ts │ ├── use-scroll-to.ts │ ├── use-select-store.ts │ ├── use-site-audio.ts │ ├── use-state-to-ref.ts │ ├── use-video-resume.ts │ ├── use-webgl.ts │ └── useScrollControl.ts ├── lib │ ├── audio │ │ ├── constants.ts │ │ └── index.ts │ ├── posthog.ts │ └── subscribable │ │ └── index.ts ├── service │ ├── basehub │ │ ├── fragments.ts │ │ └── index.ts │ └── supabase │ │ ├── client.ts │ │ └── server.ts ├── shaders │ ├── material-characters │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-flow │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-global-shader │ │ ├── fragment.glsl │ │ ├── index.tsx │ │ └── vertex.glsl │ ├── material-net │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-not-found │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-postprocessing │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-screen │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-solid-reveal │ │ ├── fragment.glsl │ │ ├── index.ts │ │ └── vertex.glsl │ ├── material-steam │ │ ├── fragment.glsl │ │ ├── index.ts │ │ ├── perlin.jpg │ │ └── vertex.glsl │ └── utils │ │ ├── basic-light.glsl │ │ └── value-remap.glsl ├── store │ ├── arcade-store.ts │ └── minigame-store.ts ├── styles │ └── globals.css ├── utils │ ├── animations.ts │ ├── argentina-time.ts │ ├── cn.ts │ ├── debounce.ts │ ├── double-fbo.ts │ ├── format-date.ts │ ├── is-in-path.ts │ └── math │ │ ├── easings.ts │ │ └── interpolation.ts └── workers │ ├── contact-worker.tsx │ └── loading-worker.tsx ├── tailwind.config.ts ├── tsconfig.json ├── vercel.json └── vercel.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # public files 43 | /public/robots.txt 44 | /public/sitemap.xml 45 | .env*.local 46 | 47 | 48 | # BaseHub 49 | .basehub 50 | 51 | # million lint 52 | .million 53 | 54 | # Sentry Config File 55 | .env.sentry-build-plugin 56 | 57 | .env.local -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.frag 2 | **/*.vert -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": false, 4 | "arrowParens": "always", 5 | "tabWidth": 2, 6 | "printWidth": 80, 7 | "trailingComma": "none", 8 | "endOfLine": "auto", 9 | "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-glsl"], 10 | "overrides": [ 11 | { 12 | "files": ["*.frag", "*.vert", "*.glsl"], 13 | "options": { 14 | "parser": "glsl-parser" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "tailwindcss.tailwindcss-intellisense", 5 | "esbenp.prettier-vscode", 6 | "stylelint.vscode-stylelint", 7 | "phoenisx.cssvar", 8 | "geforcelegend.vscode-glsl" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.fixAll.stylelint": "explicit" 7 | }, 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "typescript", 12 | "typescriptreact" 13 | ], 14 | "typescript.tsdk": "node_modules/typescript/lib", 15 | "typescript.enablePromptUseWorkspaceTsdk": true, 16 | "stylelint.validate": ["css", "scss"], 17 | "css.validate": false, 18 | "less.validate": false, 19 | "scss.validate": false, 20 | "[css]": { 21 | "editor.formatOnSave": true 22 | }, 23 | "[scss]": { 24 | "editor.formatOnSave": true 25 | }, 26 | "[less]": { 27 | "editor.formatOnSave": true 28 | }, 29 | "[glsl]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basement Studio Website 2025 2 | 3 | ![Basement Studio Website 2025](./public/readme/hero.gif) 4 | 5 | ## A digital studio & branding powerhouse making cool shit that performs. 6 | 7 | We partner with the world’s most ambitious startups, scale-ups and brands to unlock their true potential and growth through the convergence of creativity, design, and technology. Follow us on [X](https://twitter.com/basementstudio) and [Instagram](https://www.instagram.com/basementdotstudio) to see our latest work. 8 | 9 | Check out the live website at [basement.studio](https://basement.studio) 10 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | const { defineConfig } = require("eslint/config") 3 | 4 | const typescriptPlugin = require("@typescript-eslint/eslint-plugin") 5 | const prettierPlugin = require("eslint-plugin-prettier") 6 | const simpleImportSortPlugin = require("eslint-plugin-simple-import-sort") 7 | const nextPlugin = require("@next/eslint-plugin-next") 8 | const reactHooksPlugin = require("eslint-plugin-react-hooks") 9 | const typescriptParser = require("@typescript-eslint/parser") 10 | 11 | module.exports = defineConfig([ 12 | { 13 | files: ["**/*.{js,jsx,ts,tsx}"], 14 | plugins: { 15 | "@typescript-eslint": typescriptPlugin, 16 | prettier: prettierPlugin, 17 | "simple-import-sort": simpleImportSortPlugin, 18 | "@next/next": nextPlugin, 19 | "react-hooks": reactHooksPlugin 20 | }, 21 | languageOptions: { 22 | parser: typescriptParser 23 | }, 24 | rules: { 25 | semi: "off", 26 | "arrow-body-style": "off", 27 | "prefer-arrow-callback": "off", 28 | 29 | "react-hooks/exhaustive-deps": "warn", 30 | 31 | "prettier/prettier": "error", 32 | "simple-import-sort/imports": "error", 33 | "simple-import-sort/exports": "error" 34 | } 35 | } 36 | ]) 37 | -------------------------------------------------------------------------------- /instrumentation.ts: -------------------------------------------------------------------------------- 1 | export function register() { 2 | // No-op for initialization 3 | } 4 | 5 | const isNodejs = process.env.NEXT_RUNTIME === "nodejs" 6 | const isPostHogEnabled = process.env.NEXT_PUBLIC_POSTHOG_ENABLED === "true" 7 | 8 | export const onRequestError = async (err: Error, request: Request) => { 9 | if (isNodejs && isPostHogEnabled) { 10 | const { getPostHogServer } = await import("./src/lib/posthog") 11 | const posthog = getPostHogServer() 12 | 13 | let distinctId = null 14 | if (request.headers.get("cookie")) { 15 | const cookieString = request.headers.get("cookie") 16 | const postHogCookieMatch = cookieString?.match( 17 | /ph_phc_.*?_posthog=([^;]+)/ 18 | ) 19 | 20 | if (postHogCookieMatch && postHogCookieMatch[1]) { 21 | try { 22 | const decodedCookie = decodeURIComponent(postHogCookieMatch[1]) 23 | const postHogData = JSON.parse(decodedCookie) 24 | distinctId = postHogData.distinct_id 25 | } catch (e) { 26 | console.error("Error parsing PostHog cookie:", e) 27 | } 28 | } 29 | } 30 | 31 | posthog.captureException(err, distinctId || undefined) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteUrl: "https://basement.studio", 3 | 4 | generateIndexSitemap: false, 5 | generateRobotsTxt: true, 6 | 7 | exclude: [] 8 | } 9 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | 3 | const config = { 4 | plugins: { 5 | "postcss-import": {}, 6 | "tailwindcss/nesting": "postcss-nesting", 7 | tailwindcss: {} 8 | } 9 | } 10 | 11 | export default config 12 | -------------------------------------------------------------------------------- /public/basis-transcoder/basis_transcoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/website-2k25/87e1f851dac7205ea7fa4cb3bd19163610979f17/public/basis-transcoder/basis_transcoder.wasm -------------------------------------------------------------------------------- /public/fonts/flauta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/website-2k25/87e1f851dac7205ea7fa4cb3bd19163610979f17/public/fonts/flauta.ttf -------------------------------------------------------------------------------- /public/images/logobasement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/website-2k25/87e1f851dac7205ea7fa4cb3bd19163610979f17/public/images/logobasement.png -------------------------------------------------------------------------------- /public/images/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/website-2k25/87e1f851dac7205ea7fa4cb3bd19163610979f17/public/images/tile.png -------------------------------------------------------------------------------- /public/readme/hero.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basementstudio/website-2k25/87e1f851dac7205ea7fa4cb3bd19163610979f17/public/readme/hero.gif -------------------------------------------------------------------------------- /src/actions/contact-form/index.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { generateEmailTemplate } from "./template" 4 | 5 | interface ContactFormData { 6 | name: string 7 | company: string 8 | email: string 9 | budget?: string 10 | message: string 11 | } 12 | 13 | export async function submitContactForm(formData: ContactFormData) { 14 | try { 15 | const html = generateEmailTemplate(formData) 16 | 17 | const resendRes = await fetch("https://api.resend.com/emails", { 18 | method: "POST", 19 | headers: { 20 | "Content-Type": "application/json", 21 | Authorization: `Bearer ${process.env.RESEND_API_KEY}` 22 | }, 23 | body: JSON.stringify({ 24 | from: "hello@basement.studio", 25 | to: ["sales@basement.studio"], 26 | subject: `${formData.name} - ${formData.company} | Contact Us `, 27 | html 28 | }) 29 | }) 30 | 31 | if (!resendRes.ok) { 32 | throw new Error("Failed to send email") 33 | } 34 | 35 | return { success: true } 36 | } catch (error) { 37 | console.error("Error submitting form:", error) 38 | return { success: false, error: "Failed to submit form" } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/actions/laboratory-fetch/index.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { basehub } from "basehub" 4 | import { fragmentOn } from "basehub" 5 | 6 | import { IMAGE_FRAGMENT } from "@/service/basehub/fragments" 7 | 8 | const query = fragmentOn("Query", { 9 | pages: { 10 | laboratory: { 11 | projectList: { 12 | items: { 13 | _title: true, 14 | url: true, 15 | description: true, 16 | cover: IMAGE_FRAGMENT 17 | } 18 | } 19 | } 20 | } 21 | }) 22 | 23 | export const fetchLaboratory = async () => { 24 | const res = await basehub().query(query) 25 | 26 | return res.pages.laboratory 27 | } 28 | 29 | export type QueryType = fragmentOn.infer 30 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/basehub.ts: -------------------------------------------------------------------------------- 1 | import { client } from "@/service/basehub" 2 | import { IMAGE_FRAGMENT, VIDEO_FRAGMENT } from "@/service/basehub/fragments" 3 | 4 | export const fetchHomepage = async () => { 5 | const homepage = await client().query({ 6 | pages: { 7 | homepage: { 8 | intro: { 9 | title: { 10 | json: { 11 | content: true 12 | } 13 | }, 14 | subtitle: { 15 | json: { 16 | content: true 17 | } 18 | } 19 | }, 20 | capabilities: { 21 | _title: true, 22 | intro: { 23 | json: { 24 | content: true 25 | } 26 | } 27 | }, 28 | featuredProjects: { 29 | projectList: { 30 | items: { 31 | _title: true, 32 | excerpt: true, 33 | 34 | project: { 35 | _slug: true, 36 | cover: IMAGE_FRAGMENT, 37 | categories: { 38 | _title: true 39 | }, 40 | coverVideo: VIDEO_FRAGMENT 41 | }, 42 | cover: IMAGE_FRAGMENT, 43 | coverVideo: VIDEO_FRAGMENT 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | company: { 50 | clients: { 51 | clientList: { 52 | items: { 53 | _id: true, 54 | _title: true, 55 | logo: true, 56 | website: true 57 | } 58 | } 59 | }, 60 | projects: { 61 | projectCategories: { 62 | items: { 63 | _title: true, 64 | description: true, 65 | subCategories: { 66 | items: { 67 | _title: true 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | 76 | return homepage 77 | } 78 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/brands.tsx: -------------------------------------------------------------------------------- 1 | import { BrandsDesktop } from "@/components/brands" 2 | 3 | import { Scalars } from "../../../../.basehub/schema" 4 | import { BrandsMobile } from "./brands-mobile" 5 | import type { QueryType } from "./query" 6 | 7 | export type Brand = { 8 | _id: Scalars["String"] 9 | _title: Scalars["String"] 10 | logo: Scalars["String"] | null 11 | website: Scalars["String"] | null 12 | } 13 | 14 | export const Brands = ({ data }: { data: QueryType }) => { 15 | const brands = 16 | data.company.clients?.clientList.items.filter((c) => c.logo) ?? [] 17 | 18 | // Ensure we have a number of brands that's a multiple of 3 for the mobile grid 19 | const mobileBrands = [...brands] 20 | while (mobileBrands.length % 3 !== 0) { 21 | const randomIndex = Math.floor(Math.random() * mobileBrands.length) 22 | mobileBrands.splice(randomIndex, 1) 23 | } 24 | 25 | return ( 26 | <> 27 | 28 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/capabilities.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@/components/primitives/link" 2 | import { RichText } from "@/components/primitives/rich-text" 3 | 4 | import { QueryType } from "./query" 5 | 6 | export const Capabilities = ({ data }: { data: QueryType }) => { 7 | const capabilities = data.pages.homepage.capabilities 8 | const categories = data.company.projects.projectCategories.items 9 | 10 | return ( 11 |
12 |

13 | {capabilities._title} 14 |

15 | 16 |
17 | {capabilities.intro?.json?.content} 18 |
19 | 20 |
21 |
22 | {categories.map((c) => ( 23 |
27 |

28 | 31 | {c._title} 32 | 33 |

34 | 35 |

36 | {c.description} 37 |

38 | 39 |
40 | {c.subCategories.items.map((s) => ( 41 |

46 | {s._title} 47 |

48 | ))} 49 |
50 |
51 | ))} 52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/intro.tsx: -------------------------------------------------------------------------------- 1 | import { RichText } from "basehub/react-rich-text" 2 | 3 | import { QueryType } from "./query" 4 | 5 | export const Intro = ({ data }: { data: QueryType }) => { 6 | return ( 7 |
8 |
9 |
10 | ( 13 |

14 | {children} 15 |

16 | ) 17 | }} 18 | > 19 | {data.pages.homepage.intro.title?.json.content} 20 |
21 |
22 |
23 | ( 26 |

27 | {children} 28 |

29 | ) 30 | }} 31 | > 32 | {data.pages.homepage.intro.subtitle?.json.content} 33 |
34 |
35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | 3 | import { Contact } from "@/components/layout/contact" 4 | 5 | import { fetchHomepage } from "./basehub" 6 | import { Brands } from "./brands" 7 | import { Capabilities } from "./capabilities" 8 | import { FeaturedProjects } from "./featured-projects" 9 | import { Intro } from "./intro" 10 | 11 | export const metadata: Metadata = { 12 | title: { 13 | absolute: "basement.studio | We make cool shit that performs." 14 | }, 15 | alternates: { 16 | canonical: "https://basement.studio" 17 | } 18 | } 19 | 20 | const Homepage = async () => { 21 | const data = await fetchHomepage() 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | ) 32 | } 33 | 34 | export default Homepage 35 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/query.ts: -------------------------------------------------------------------------------- 1 | import { fragmentOn } from "basehub" 2 | 3 | import { IMAGE_FRAGMENT, VIDEO_FRAGMENT } from "@/service/basehub/fragments" 4 | 5 | export const query = fragmentOn("Query", { 6 | pages: { 7 | homepage: { 8 | intro: { 9 | title: { 10 | json: { 11 | content: true 12 | } 13 | }, 14 | subtitle: { 15 | json: { 16 | content: true 17 | } 18 | } 19 | }, 20 | capabilities: { 21 | _title: true, 22 | intro: { 23 | json: { 24 | content: true 25 | } 26 | } 27 | }, 28 | featuredProjects: { 29 | projectList: { 30 | items: { 31 | _title: true, 32 | excerpt: true, 33 | 34 | project: { 35 | _slug: true, 36 | cover: IMAGE_FRAGMENT, 37 | categories: { 38 | _title: true 39 | }, 40 | coverVideo: VIDEO_FRAGMENT 41 | }, 42 | cover: IMAGE_FRAGMENT, 43 | coverVideo: VIDEO_FRAGMENT 44 | } 45 | } 46 | } 47 | } 48 | }, 49 | company: { 50 | clients: { 51 | clientList: { 52 | items: { 53 | _id: true, 54 | _title: true, 55 | logo: true, 56 | website: true 57 | } 58 | } 59 | }, 60 | projects: { 61 | projectCategories: { 62 | items: { 63 | _title: true, 64 | description: true, 65 | subCategories: { 66 | items: { 67 | _title: true 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | 76 | export type QueryType = fragmentOn.infer 77 | -------------------------------------------------------------------------------- /src/app/(pages)/(home)/showcase-image.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ImageWithVideoOverlay } from "@/components/primitives/image-with-video-overlay" 4 | import { Link } from "@/components/primitives/link" 5 | import { useMedia } from "@/hooks/use-media" 6 | import { useCursor } from "@/hooks/use-mouse" 7 | 8 | import { QueryType } from "./query" 9 | 10 | interface ShowcaseImageProps { 11 | project: QueryType["pages"]["homepage"]["featuredProjects"]["projectList"]["items"][0] 12 | } 13 | 14 | export const ShowcaseImage = ({ project }: ShowcaseImageProps) => { 15 | const setCursor = useCursor() 16 | const isCursorHover = useMedia("(hover: hover)") 17 | 18 | // TODO: Add required in basehub to avoid this 19 | if (!project.cover) return null 20 | 21 | return ( 22 | { 25 | if (isCursorHover) setCursor("zoom-in", "View Project") 26 | }} 27 | onMouseLeave={() => setCursor("default", null)} 28 | className="block focus-visible:!ring-offset-0" 29 | aria-label={`View ${project._title ?? "Untitled"}`} 30 | > 31 |
32 | 37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/app/(pages)/blog/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlogList } from "@/components/blog/list" 2 | 3 | import { fetchCategoriesNonEmpty } from "../basehub" 4 | 5 | type Params = Promise<{ slug: string[] }> 6 | 7 | export const experimental_ppr = true 8 | 9 | const BlogIndexPage = async (props: { params: Params }) => { 10 | const params = await props.params 11 | 12 | return 13 | } 14 | 15 | // pre build all the categories 16 | export const generateStaticParams = async () => { 17 | const categories = await fetchCategoriesNonEmpty() 18 | 19 | categories.unshift({ 20 | _title: "Home", 21 | _slug: "" 22 | }) 23 | 24 | return categories.map((category) => ({ 25 | slug: [category._slug] 26 | })) 27 | } 28 | 29 | export default BlogIndexPage 30 | -------------------------------------------------------------------------------- /src/app/(pages)/blog/hero.tsx: -------------------------------------------------------------------------------- 1 | import { client } from "@/service/basehub" 2 | 3 | const fetchPostsLength = async () => { 4 | const res = await client().query({ 5 | pages: { 6 | blog: { 7 | posts: { _meta: { totalCount: true } } 8 | } 9 | } 10 | }) 11 | 12 | return res.pages.blog.posts._meta.totalCount 13 | } 14 | 15 | export async function Hero() { 16 | const length = await fetchPostsLength() 17 | 18 | return ( 19 |
20 |

21 | Blog 22 |

23 |

24 | {length} 25 |

26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/(pages)/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | 3 | import { Categories } from "@/components/blog/categories" 4 | 5 | import { Featured } from "./featured" 6 | import { Hero } from "./hero" 7 | 8 | export const metadata: Metadata = { 9 | title: "Blog", 10 | alternates: { 11 | canonical: "https://basement.studio/blog" 12 | } 13 | } 14 | 15 | interface BlogLayoutProps { 16 | children: React.ReactNode 17 | } 18 | 19 | export default function BlogLayout({ children }: BlogLayoutProps) { 20 | return ( 21 | <> 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 |

30 | More News 31 |

32 | 33 |
34 | {children} 35 |
36 |
37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/app/(pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "@/components/layout/footer" 2 | import { ScrollDown } from "@/components/primitives/scroll-down" 3 | 4 | const Layout = ({ children }: { children: React.ReactNode }) => ( 5 | <> 6 |
7 | 8 | {children} 9 |
10 |