diff --git a/apps/react-three-org/src/app.tsx b/apps/react-three-org/src/app.tsx index d9390c4..137c103 100644 --- a/apps/react-three-org/src/app.tsx +++ b/apps/react-three-org/src/app.tsx @@ -1,54 +1,61 @@ -import { useEffect, useMemo, useState } from 'react' -import { Loading } from './loading.js' -import { useQuery } from '@tanstack/react-query' -import { PackageCard } from '@/components/package-card' -import { ProjectConfigurator } from '@/components/project-configurator' -import { NavBar } from '@/components/nav-bar' -import { packages, tools, PackageIDs, ToolIDs } from '@/lib/packages' -import { BackgroundAnimation } from '@/components/background-animation' -import { CogIcon, PackageIcon } from 'lucide-react' +import { PackageIDs, packages, ToolIDs, tools } from '@/lib/packages' +import { useState } from 'react' +import { ProjectConfigurator, FilterType, GithubRepo, SelectBox } from './components/redesign-ui' import { Toaster } from 'sonner' -import { SelectionSection } from './components/selection-section.js' +import { Hero } from './components/redesign-ui/hero' +import { ListItem } from './components/redesign-ui/list-item' +import { useIsMobile } from './hooks/use-mobile' const searchParams = new URLSearchParams(location.search) -const sessionAccessTokenKey = 'access_token' -const sessionAccessToken = sessionStorage.getItem(sessionAccessTokenKey) export function App() { const [state, setState] = useState(() => searchParams.get('state')) const [selectedPackages, setSelectedPackages] = useState([]) const [selectedTools, setSelectedTools] = useState(['triplex']) + const allPackagesSelected = packages.every((pkg) => selectedPackages.includes(pkg.id)) + const allToolsSelected = tools.every((tool) => selectedTools.includes(tool.id)) + + const isMobile = useIsMobile() if (state != null) { return } return ( -
- -
-
-

React Three

-

- Building 3D experiences with the React Three Ecosystem -

- -
- - {/* Visual separator using space instead of a border */} -
- - + +
+ { + setSelectedPackages(selected ? packages.map((pkg) => pkg.id) : []) + }} + onSelectAllTools={(selected) => { + setSelectedTools(selected ? tools.map((tool) => tool.id) : []) + }} /> -
+
+ {isMobile && ( +
+ +
+ )} {packages.map((pkg) => ( - ))} -
- - {/* Visual separator using space instead of a border */} -
- - -
+ {isMobile && ( +
+ +
+ )} {tools.map((pkg) => ( - ))}
- - { - const integrations: any = {} - const selections = [...selectedPackages, ...selectedTools] - for (const integration of selections) { - integrations[integration] = true - } - - setState(btoa(JSON.stringify(integrations))) - }} - selections={[...selectedPackages, ...selectedTools]} - />
+ { + const integrations: any = {} + const selections = [...selectedPackages, ...selectedTools] + for (const integration of selections) { + integrations[integration] = true + } + + setState(btoa(JSON.stringify(integrations))) + }} + selections={[...selectedPackages, ...selectedTools]} + /> ) } - -function GithubRepo({ state }: { state: string }) { - const code = useMemo(() => searchParams.get('code'), []) - const { - isPending: isPendingAccessToken, - error: errorAccessToken, - data: accessTokenData, - } = useQuery({ - retry: false, - enabled: sessionAccessToken == null, - queryKey: ['oauth', code], - queryFn: async () => { - if (code == null) { - //promise never - return new Promise<{ token: string }>(() => {}) - } - const response = await fetch(new URL(`/oauth?code=${code}`, import.meta.env.VITE_SERVER_URL)) - if (!response.ok) { - throw new Error(response.statusText) - } - return response.json() as any as { token: string } - }, - }) - useEffect(() => { - if (accessTokenData == null) { - return - } - sessionStorage.setItem(sessionAccessTokenKey, accessTokenData.token) - }, [accessTokenData?.token]) - const accessToken = sessionAccessToken ?? accessTokenData?.token - const { - data: repoData, - isPending: isPendingRepo, - error: repoError, - } = useQuery({ - retry: false, - enabled: accessToken != null, - queryKey: ['repo', accessToken], - queryFn: async () => { - const response = await fetch(new URL('/repo', import.meta.env.VITE_SERVER_URL), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ token: accessToken!, ...JSON.parse(atob(decodeURIComponent(state))) }), - }) - if (!response.ok) { - throw new Error(response.statusText) - } - return response.json() as any as { url: string } - }, - }) - useEffect(() => { - if (repoData?.url == null) { - return - } - setTimeout(() => (location.href = repoData.url), 1000) - }, [repoData?.url]) - if (code == null) { - location.href = `https://github.com/login/oauth/authorize?client_id=${ - import.meta.env.VITE_CLIENT_ID - }&redirect_uri=${import.meta.env.VITE_REDIRECT_URL}&state=${state}&scope=user%20repo%20workflow` - return null - } - if (sessionAccessToken == null && isPendingAccessToken) { - return - } - if (errorAccessToken != null) { - return errorAccessToken.message - } - if (isPendingRepo) { - return - } - if (repoError) { - return repoError.message - } - return -} diff --git a/apps/react-three-org/src/components/redesign-ui/filter-type.tsx b/apps/react-three-org/src/components/redesign-ui/filter-type.tsx new file mode 100644 index 0000000..f2fe368 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/filter-type.tsx @@ -0,0 +1,59 @@ +import { type Package, PackageIDs, packages, ToolIDs, tools } from '@/lib/packages' +import { SelectBox } from './select-box' +import { useIsMobile } from '@/hooks/use-mobile' + +interface FilterTypeProps { + selectedPackages: PackageIDs[] + selectedTools: ToolIDs[] + onSelectAllPackages: (selected: boolean) => void + onSelectAllTools: (selected: boolean) => void +} + +export const FilterType = ({ + selectedPackages, + selectedTools, + onSelectAllPackages, + onSelectAllTools, +}: FilterTypeProps) => { + const allPackagesSelected = packages.every((pkg) => selectedPackages.includes(pkg.id)) + const allToolsSelected = tools.every((tool) => selectedTools.includes(tool.id)) + + return ( +
+
+
+

Filter Options

+

+ / SELECT ALL +

+ +
+ +
+ +
+ +
+
+
+
+ ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/filter.tsx b/apps/react-three-org/src/components/redesign-ui/filter.tsx new file mode 100644 index 0000000..b6487e5 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/filter.tsx @@ -0,0 +1,33 @@ +import { packages, tools } from '@/lib/packages' +import { Package } from '@/lib/packages' +import { SelectBox } from './select-box' + +//TODO + +export const Filter = ({ + handleSelectAll, + selectedPackages, + visibleTypes, +}: { + handleSelectAll: () => void + selectedPackages: Package[] + visibleTypes: { packages: boolean; tools: boolean } +}) => { + const allVisible = [...(visibleTypes.packages ? packages : []), ...(visibleTypes.tools ? tools : [])] + const allSelected = allVisible.length > 0 && selectedPackages.length === allVisible.length + + return ( +
+
+

SELECT WHAT POWERS YOUR PROJECT.

+
+

A - Z

+
+ +

SELECT ALL

+
+
+
+
+ ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/github-repo.tsx b/apps/react-three-org/src/components/redesign-ui/github-repo.tsx new file mode 100644 index 0000000..4530c7a --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/github-repo.tsx @@ -0,0 +1,86 @@ +import { useEffect, useMemo } from 'react' +import { useQuery } from '@tanstack/react-query' +import { Loading } from '@/loading' + +const searchParams = new URLSearchParams(location.search) +const sessionAccessTokenKey = 'access_token' +const sessionAccessToken = sessionStorage.getItem(sessionAccessTokenKey) + +export function GithubRepo({ state }: { state: string }) { + const code = useMemo(() => searchParams.get('code'), []) + const { + isPending: isPendingAccessToken, + error: errorAccessToken, + data: accessTokenData, + } = useQuery({ + retry: false, + enabled: sessionAccessToken == null, + queryKey: ['oauth', code], + queryFn: async () => { + if (code == null) { + //promise never + return new Promise<{ token: string }>(() => {}) + } + const response = await fetch(new URL(`/oauth?code=${code}`, import.meta.env.VITE_SERVER_URL)) + if (!response.ok) { + throw new Error(response.statusText) + } + return response.json() as any as { token: string } + }, + }) + useEffect(() => { + if (accessTokenData == null) { + return + } + sessionStorage.setItem(sessionAccessTokenKey, accessTokenData.token) + }, [accessTokenData?.token]) + const accessToken = sessionAccessToken ?? accessTokenData?.token + const { + data: repoData, + isPending: isPendingRepo, + error: repoError, + } = useQuery({ + retry: false, + enabled: accessToken != null, + queryKey: ['repo', accessToken], + queryFn: async () => { + const response = await fetch(new URL('/repo', import.meta.env.VITE_SERVER_URL), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token: accessToken!, ...JSON.parse(atob(decodeURIComponent(state))) }), + }) + if (!response.ok) { + throw new Error(response.statusText) + } + return response.json() as any as { url: string } + }, + }) + useEffect(() => { + if (repoData?.url == null) { + return + } + setTimeout(() => (location.href = repoData.url), 1000) + }, [repoData?.url]) + if (code == null) { + location.href = `https://github.com/login/oauth/authorize?client_id=${ + import.meta.env.VITE_CLIENT_ID + }&redirect_uri=${import.meta.env.VITE_REDIRECT_URL}&state=${state}&scope=user%20repo%20workflow` + return null + } + if (sessionAccessToken == null && isPendingAccessToken) { + return + } + if (errorAccessToken != null) { + return errorAccessToken.message + } + if (isPendingRepo) { + return + } + if (repoError) { + return repoError.message + } + return + } + \ No newline at end of file diff --git a/apps/react-three-org/src/components/redesign-ui/hero.tsx b/apps/react-three-org/src/components/redesign-ui/hero.tsx new file mode 100644 index 0000000..8860a09 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/hero.tsx @@ -0,0 +1,66 @@ +import { useThree, useFrame } from '@react-three/fiber' +import { Canvas } from '@react-three/fiber' +import { NavBar } from './navbar' +import { useMemo, useEffect } from 'react' +import * as THREE from 'three' +import { createShaderMaterial } from './shader-material' + +export const Hero = () => { + return ( +
+
+ +
+

react-three

+

BUILDING COOL 3D EXPERIENCES WITH THE REACT THREE ECOSYSTEM.

+
+
+
+ + + + + +
+ Interactive 3D visualization demonstrating React Three capabilities +
+
+
+ ) +} + +const CameraPlane = () => { + const { camera, size } = useThree() + const shaderMaterial = useMemo(() => createShaderMaterial(), []) + + const planeZ = 1 + const distanceFromCamera = camera.position.z - planeZ + const fovRadians = THREE.MathUtils.degToRad(camera instanceof THREE.PerspectiveCamera ? camera.fov : 75) + const height = 2 * Math.tan(fovRadians / 2) * distanceFromCamera + const width = height * (size.width / size.height) + + useEffect(() => { + if (shaderMaterial.uniforms?.uResolution) { + shaderMaterial.uniforms.uResolution.value.set(size.width, size.height, 1) + } + }, [size, shaderMaterial]) + + useFrame(({ clock }) => { + if (shaderMaterial.uniforms?.uTime) { + shaderMaterial.uniforms.uTime.value = clock.getElapsedTime() + } + }) + + return ( + + + + + ) +} + + diff --git a/apps/react-three-org/src/components/redesign-ui/index.ts b/apps/react-three-org/src/components/redesign-ui/index.ts new file mode 100644 index 0000000..6124b6c --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/index.ts @@ -0,0 +1,7 @@ +export * from './filter' +export * from './filter-type' +export * from './link' +export * from './navbar' +export * from './project-configurator' +export * from './select-box' +export * from './github-repo' diff --git a/apps/react-three-org/src/components/redesign-ui/link.tsx b/apps/react-three-org/src/components/redesign-ui/link.tsx new file mode 100644 index 0000000..363c1c2 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/link.tsx @@ -0,0 +1,29 @@ +import { cn } from "@/lib/utils" + +export const Link = ({ + children, + href, + isExternal = false, + 'aria-label': ariaLabel, + className, +}: { + children: React.ReactNode + href: string + isExternal?: boolean + 'aria-label'?: string + className?: string +}) => { + const externalProps = isExternal + ? { + target: '_blank', + rel: 'noopener noreferrer', + 'aria-label': ariaLabel || undefined, + } + : {} + + return ( + + {children} + + ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/list-item.tsx b/apps/react-three-org/src/components/redesign-ui/list-item.tsx new file mode 100644 index 0000000..4cfbaf6 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/list-item.tsx @@ -0,0 +1,49 @@ +import { Package } from '@/lib/packages' +import { cn } from '@/lib/utils' +import { SelectBox } from './select-box' +import { Link } from './link' + +interface PackageCardProps { + package: Package + isSelected: boolean + onToggle: () => void +} + +export const ListItem = ({ package: pkg, isSelected, onToggle }: PackageCardProps) => { + return ( + + ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/navbar.tsx b/apps/react-three-org/src/components/redesign-ui/navbar.tsx new file mode 100644 index 0000000..68d8879 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/navbar.tsx @@ -0,0 +1,59 @@ +import { Link } from "./link" + +export const NavBar = () => { + return ( + + ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/project-configurator.tsx b/apps/react-three-org/src/components/redesign-ui/project-configurator.tsx new file mode 100644 index 0000000..ada0723 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/project-configurator.tsx @@ -0,0 +1,91 @@ +import { Copy } from 'lucide-react' +import { generate } from '@react-three/create' +import JSZip from 'jszip' +import { toast } from 'sonner' + +interface ProjectConfiguratorProps { + selections: string[] + createGithubRepo: () => void +} + +export const ProjectConfigurator = ({ selections, createGithubRepo }: ProjectConfiguratorProps) => { + const command = `npm create @react-three ${selections.length > 0 ? '-- ' : ''}${selections + .map((id) => `--${id}`) + .join(' ')}` + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(command) + toast.success('Command copied to clipboard') + } catch (err) { + toast.error('Failed to copy command') + } + } + + const downloadProject = async () => { + const element = document.createElement('a') + const options: any = {} + for (const selection of selections) { + options[selection] = true + } + const files = generate(options) + console.log(files) + const zip = new JSZip() + // Write each file into the zip + for (const [path, file] of Object.entries(files)) { + const parts = path.split('/').filter(Boolean) + + let currentFolder = zip + // Create nested folders if needed + for (let i = 0; i < parts.length - 1; i++) { + currentFolder = currentFolder.folder(parts[i]!)! + } + // Add the file + let content: string | Blob + if (file.type === 'text') { + content = file.content + } else { + // For remote files, we'll need to fetch them first + const response = await fetch(file.url) + content = await response.blob() + } + currentFolder.file(parts[parts.length - 1]!, content) + } + const content = await zip.generateAsync({ type: 'blob' }) + element.href = URL.createObjectURL(content) + element.download = 'react-three-app.zip' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) + } + + return ( +
+
+

{'>_ ' + command}

+ +
+
+

+ {selections.length} {selections.length === 1 ? 'package' : 'packages'} selected +

+
+ + +
+
+
+ ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/select-box.tsx b/apps/react-three-org/src/components/redesign-ui/select-box.tsx new file mode 100644 index 0000000..40ac3e8 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/select-box.tsx @@ -0,0 +1,8 @@ +export const SelectBox = ({ selected }: { selected?: boolean }) => { + return ( + + + {selected && } + + ) +} diff --git a/apps/react-three-org/src/components/redesign-ui/shader-material.tsx b/apps/react-three-org/src/components/redesign-ui/shader-material.tsx new file mode 100644 index 0000000..f9ef2b9 --- /dev/null +++ b/apps/react-three-org/src/components/redesign-ui/shader-material.tsx @@ -0,0 +1,149 @@ +import * as THREE from 'three' + +export const createShaderMaterial = () => { + return new THREE.ShaderMaterial({ + uniforms: { + uTime: { value: 0 }, + uResolution: { value: new THREE.Vector3(1, 1, 1) }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform float uTime; + uniform vec3 uResolution; + varying vec2 vUv; + + mat4 RotationMatrixX(float angle){ + return mat4(1.0, 0.0, 0.0, 0.0, + 0.0, cos(angle), -sin(angle), 0.0, + 0.0, sin(angle), cos(angle), 0.0, + 0.0, 0.0, 0.0, 1.0); + } + + mat4 RotationMatrixY(float angle){ + return mat4(cos(angle), 0.0, sin(angle), 0.0, + 0.0, 1.0, 0.0, 0.0, + -sin(angle), 0.0, cos(angle), 0.0, + 0.0, 0.0, 0.0, 1.0); + } + + float sdCircle(vec2 st, vec2 pos, float radius){ + return length(st - pos) + radius; + } + + float sinTheta(float theta){ + return sin(theta * 3.14 / 180.0); + } + + float cosTheta(float theta){ + return cos(theta * 3.14 / 180.0); + } + + vec3 getRotation(int index){ + vec3 angle[7]; + angle[0] = vec3(1.0, 0.5, 0.0); + angle[1] = vec3(-1.0, 0.5, 0.0); + angle[2] = vec3(0.0, 2.0, 0.0); + return angle[index]; + } + + float getNeonCircle(float circle, float radius, float brightness){ + circle -= radius; + circle = abs(circle); + return circle = brightness / circle; + } + + float random(vec2 st) { + return fract(sin(dot(st.xy, + vec2(12.9898,79.321)))* + 51758.54); + } + + float noise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix(a, b, u.x) + + (c - a)* u.y * (1.0 - u.x) + + (d - b) * u.x * u.y; + } + + float fbm(vec2 st) { + int octaves = 2; + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100.0); + mat2 rot = mat2(cos(0.5), sin(0.5), + -sin(0.5), cos(0.5)); + for (int i = 0; i < octaves; i++) { + v += a * noise(st); + st = rot * st * 2.0 + shift; + a *= 0.5; + } + return v; + } + + void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 st = (fragCoord.xy * 2.0 - uResolution.xy) / uResolution.y; + vec3 finalColor = vec3(0.0); + + vec2 q = vec2(0.0); + q.x = fbm(st + 0.00*uTime); + q.y = fbm(st + vec2(1.0)); + vec2 r = vec2(0.0); + r.x = fbm(st + 1.0*q + vec2(1.7,9.2)+ 0.15*uTime); + r.y = fbm(st + 1.0*q + vec2(8.3,2.8)+ 0.126*uTime); + float f = fbm(st+r); + + float signed = 1.0; + for (float i = 0.0; i < 3.0; i++){ + vec3 col = vec3(0.0); + vec4 st0 = vec4(st, 0.0, 1.0); + vec3 angle = getRotation(int(i)); + st0 *= RotationMatrixX(angle.x); + st0 *= RotationMatrixY(angle.y); + + float radius = 0.6; + float circle = sdCircle(st0.xy, vec2(0.0), radius); + circle = getNeonCircle(circle, 1.0, 0.01); + + col +=circle; + float timeCoef = uTime * 2.0 + i - signed; + float theta = 90.0 * timeCoef * signed; + float theta2 = 90.0 * (timeCoef + 1.2) * signed; + float cosAngle = st0.x * cosTheta(theta); + float sinAngle = st0.y * sinTheta(theta); + + col *= (cosAngle + sinAngle); + + finalColor = max(finalColor + col, finalColor); + signed *= -1.0; + } + + finalColor *= vec3(1.0, 1.0, 1.0); + fragColor = vec4(finalColor, 1.0); + } + + void main() { + vec2 fragCoord = vUv * uResolution.xy; + vec4 fragColor; + mainImage(fragColor, fragCoord); + gl_FragColor = fragColor; + } + `, + side: THREE.DoubleSide, + transparent: true + }); + }; \ No newline at end of file diff --git a/apps/react-three-org/src/global.css b/apps/react-three-org/src/global.css index ff05ccd..c0f24bd 100644 --- a/apps/react-three-org/src/global.css +++ b/apps/react-three-org/src/global.css @@ -10,9 +10,26 @@ font-display: swap; } +@font-face { + font-family: 'Geist Mono'; + src: url('./assets/fonts/GeistMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist Mono'; + src: url('./assets/fonts/GeistMono-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + :root { font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-mono: 'Geist Mono', monospace; --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); @@ -45,6 +62,12 @@ --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); + + /* Design system colors */ + --redesign-white: #FAFAFA; + --redesign-gray: #404040; + --redesign-dark: #1A1A1A; + --redesign-black: #050505; } @supports (font-variation-settings: normal) { @@ -94,9 +117,7 @@ body { } /* Add a subtle glow to text */ -h1 { - text-shadow: 0 0 10px color-mix(in oklab, white 50%, transparent); -} + button { cursor: pointer; @@ -138,6 +159,10 @@ button { --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-redesign-white: var(--redesign-white); + --color-redesign-gray: var(--redesign-gray); + --color-redesign-dark: var(--redesign-dark); + --color-redesign-black: var(--redesign-black); } .dark { @@ -186,4 +211,9 @@ button { @keyframes spinner-leaf-fade { 0%, 100% { opacity: 0; } 50% { opacity: 1; } +} + +.font-mono { + font-family: var(--font-mono); + letter-spacing: 0; } \ No newline at end of file