diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 9b944f712..faf91b29d 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -20,7 +20,7 @@ import { } from '@sd/interface'; import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState'; -import '@sd/ui/style'; +import '@sd/ui/style/style.scss'; import * as commands from './commands'; import { createUpdater } from './updater'; diff --git a/apps/landing/drizzle.config.ts b/apps/landing/drizzle.config.ts deleted file mode 100644 index 3a4184293..000000000 --- a/apps/landing/drizzle.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import 'dotenv/config'; - -import { Config } from 'drizzle-kit'; - -// TODO: Using t3 env is too damn hard, thanks JS bs -if (!process.env.DATABASE_URL) { - throw new Error('DATABASE_URL is not set'); -} - -export default { - schema: ['./src/server/db.ts'], - connectionString: process.env.DATABASE_URL -} satisfies Config; diff --git a/apps/landing/next-env.d.ts b/apps/landing/next-env.d.ts index fd36f9494..4f11a03dc 100644 --- a/apps/landing/next-env.d.ts +++ b/apps/landing/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/landing/next.config.js b/apps/landing/next.config.js index eb9280868..2606fd607 100644 --- a/apps/landing/next.config.js +++ b/apps/landing/next.config.js @@ -1,4 +1,7 @@ const { withContentlayer } = require('next-contentlayer'); +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true' +}); // Validate env on build // TODO: I wish we could do this so Vercel can warn us when we are wrong but it's too hard. // import './src/env.mjs'; @@ -8,6 +11,15 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, transpilePackages: ['@sd/ui'], + eslint: { + ignoreDuringBuilds: true + }, + typescript: { + ignoreBuildErrors: true + }, + experimental: { + optimizePackageImports: ['@sd/ui'] + }, webpack(config) { // Grab the existing rule that handles SVG imports const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); @@ -45,4 +57,4 @@ const nextConfig = { } }; -module.exports = withContentlayer(nextConfig); +module.exports = withBundleAnalyzer(withContentlayer(nextConfig)); diff --git a/apps/landing/package.json b/apps/landing/package.json index 107eba1a7..2dd930982 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -7,12 +7,11 @@ "start": "next start", "prod": "pnpm build && pnpm start", "lint": "next lint", - "typecheck": "contentlayer build && tsc -b", - "push": "drizzle-kit push:mysql" + "typecheck": "contentlayer build && tsc -b" }, "dependencies": { "@aws-sdk/client-ses": "^3.337.0", - "@phosphor-icons/react": "^2.0.10", + "@phosphor-icons/react": "^2.1.3", "@planetscale/database": "^1.7.0", "@react-three/drei": "^9.78.1", "@react-three/fiber": "^8.13.4", @@ -24,16 +23,17 @@ "clsx": "^1.2.1", "contentlayer": "^0.3.2", "dayjs": "^1.11.8", - "drizzle-orm": "^0.26.0", "framer-motion": "^10.11.5", + "katex": "^0.16.9", "markdown-to-jsx": "^7.2.0", "md5": "^2.3.0", - "next": "13.4.3", + "next": "13.5.4", "next-contentlayer": "^0.3.4", "react": "18.2.0", "react-burger-menu": "^3.0.9", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", "react-github-btn": "^1.4.0", "react-hook-form": "^7.47.0", "react-tsparticles": "^2.9.3", @@ -53,6 +53,7 @@ "zod": "~3.22.4" }, "devDependencies": { + "@next/bundle-analyzer": "^13.5.4", "@sd/config": "workspace:*", "@svgr/webpack": "^8.1.0", "@types/node": "~18.17.19", @@ -60,7 +61,6 @@ "@types/react-burger-menu": "^2.8.4", "@types/react-dom": "^18.2.13", "@types/three": "^0.152.1", - "drizzle-kit": "db-push", "postcss": "^8.4.31", "tailwindcss": "^3.3.3", "typescript": "^5.2.2" diff --git a/apps/landing/src/app/Background.tsx b/apps/landing/src/app/Background.tsx new file mode 100644 index 000000000..58fbc0fe9 --- /dev/null +++ b/apps/landing/src/app/Background.tsx @@ -0,0 +1,69 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { ReactNode, Suspense, useEffect, useState } from 'react'; +import { hasWebGLContext } from '~/utils/util'; + +const FADE = { + start: 300, // start fading out at 100px + end: 1300 // end fading out at 300px +}; + +const Space = dynamic(() => import('~/components/Space'), { ssr: false }); +const Bubbles = dynamic(() => import('~/components/Bubbles').then((m) => m.Bubbles), { + ssr: false +}); + +export function Background() { + const [opacity, setOpacity] = useState(0.6); + const [isWindowResizing, setIsWindowResizing] = useState(false); + const [inner, setInner] = useState(null); + + useEffect(() => { + const handleScroll = () => { + const currentScrollY = window.scrollY; + + if (currentScrollY <= FADE.start) { + setOpacity(0.6); + } else if (currentScrollY <= FADE.end) { + const range = FADE.end - FADE.start; + const diff = currentScrollY - FADE.start; + const ratio = diff / range; + setOpacity(0.6 - ratio); + } else { + setOpacity(0); + } + }; + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + useEffect(() => { + let resizeTimer: ReturnType; + const handleResize = () => { + setIsWindowResizing(true); + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + setIsWindowResizing(false); + }, 100); + }; + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + clearTimeout(resizeTimer); + }; + }, []); + + useEffect(() => { + setInner(hasWebGLContext() ? : ); + }, []); + + return ( +
+ {!isWindowResizing && inner} +
+ ); +} diff --git a/apps/landing/src/app/Downloads.tsx b/apps/landing/src/app/Downloads.tsx new file mode 100644 index 000000000..e26f8581a --- /dev/null +++ b/apps/landing/src/app/Downloads.tsx @@ -0,0 +1,211 @@ +'use client'; + +import { AndroidLogo, Globe, LinuxLogo, WindowsLogo } from '@phosphor-icons/react/dist/ssr'; +import { Apple, Github } from '@sd/assets/svgs/brands'; +import clsx from 'clsx'; +import { motion } from 'framer-motion'; +import { ComponentProps, FunctionComponent, useEffect, useState } from 'react'; +import { Tooltip } from '@sd/ui'; + +import HomeCTA from './HomeCTA'; + +const RELEASE_VERSION = 'Alpha v0.1.0'; + +interface Platform { + name: string; + os?: string; + icon: FunctionComponent; + version?: string; + links?: Array<{ name: string; arch: string }>; +} + +const platforms = { + darwin: { + name: 'macOS', + os: 'darwin', + icon: Apple, + version: '12+', + links: [ + { name: 'Intel', arch: 'x86_64' }, + { name: 'Apple Silicon', arch: 'aarch64' } + ] + }, + windows: { + name: 'Windows', + os: 'windows', + icon: WindowsLogo, + version: '10+', + links: [{ name: 'x86_64', arch: 'x86_64' }] + }, + linux: { + name: 'Linux', + os: 'linux', + icon: LinuxLogo, + version: 'AppImage', + links: [{ name: 'x86_64', arch: 'x86_64' }] + }, + android: { name: 'Android', icon: AndroidLogo, version: '10+' }, + web: { name: 'Web', icon: Globe } +} satisfies Record; + +const BASE_DL_LINK = '/api/releases/desktop/stable'; + +export function Downloads() { + const [selectedPlatform, setSelectedPlatform] = useState(null); + const currentPlatform = useCurrentPlatform(); + + const formattedVersion = (() => { + const platform = selectedPlatform ?? currentPlatform; + + if (!platform?.version) return; + if (platform.name === 'Linux') return platform.version; + + return `${platform.name} ${platform.version}`; + })(); + + return ( + <> +
+ {currentPlatform && + (() => { + const Icon = currentPlatform.icon; + const { links } = currentPlatform; + + return ( + : undefined} + text={`Download for ${currentPlatform.name}`} + onClick={() => setSelectedPlatform(currentPlatform)} + /> + ); + })()} + + } + className="z-5 relative" + text="Star on GitHub" + /> +
+ + {selectedPlatform?.links && selectedPlatform.links.length > 1 && ( +
+ {selectedPlatform.links.map(({ name, arch }) => ( + + ))} +
+ )} +

+ {RELEASE_VERSION} + {formattedVersion && ( + <> + | + {formattedVersion} + + )} +

+
+ {Object.values(platforms as Record).map((platform, i) => { + return ( + + { + if (platform.links && platform.links.length > 1) + setSelectedPlatform(platform); + }} + /> + + ); + })} +
+ + ); +} + +function useCurrentPlatform() { + const [currentPlatform, setCurrentPlatform] = useState(null); + + useEffect(() => { + import('react-device-detect').then(({ isWindows, isMacOs, isMobile }) => { + setCurrentPlatform((e) => { + if (e) return e; + + if (isWindows) { + return platforms.windows; + } else if (isMacOs) { + return platforms.darwin; + } else if (!isMobile) { + return platforms.linux; + } + + return null; + }); + }); + }, []); + + return currentPlatform; +} + +interface Props { + platform: Platform; +} +function Platform({ platform, ...props }: ComponentProps<'a'> & Props) { + const { links } = platform; + + const Outer = links + ? links.length === 1 + ? (props: any) => ( + + ) + : (props: any) => + ); +} + +export default HomeCTA; diff --git a/apps/landing/src/app/NavBar/MobileDropdown.tsx b/apps/landing/src/app/NavBar/MobileDropdown.tsx new file mode 100644 index 000000000..e00771319 --- /dev/null +++ b/apps/landing/src/app/NavBar/MobileDropdown.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { Book, Chat, DotsThreeVertical, MapPin, User } from '@phosphor-icons/react/dist/ssr'; +import { Academia, Discord, Github } from '@sd/assets/svgs/brands'; +import clsx from 'clsx'; +import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { usePathname, useRouter } from 'next/navigation'; +import { Button, Dropdown } from '@sd/ui'; +import { getWindow } from '~/utils/util'; + +import { positions } from '../careers/data'; + +export function MobileDropdown() { + const router = useRouter(); + const pathname = usePathname(); + + const link = (path: string) => rawLink(path, router, pathname); + + return ( + + + + } + className="right-4 top-2 block h-6 w-44 text-white lg:hidden" + itemsClassName="!rounded-2xl shadow-2xl shadow-black p-2 !bg-gray-850 mt-2 !border-gray-500 text-[15px]" + > + + + Join Discord + + + Repository + + + + + Roadmap + + + Team + + {/* + Pricing + */} + + Blog + + + Docs + + + Careers + {positions.length > 0 ? ( + + {positions.length} + + ) : null} + + + + ); +} + +function rawLink(path: string, router: AppRouterInstance, pathname: string) { + const selected = pathname.includes(path); + + return { + selected, + onClick: () => router.push(path), + className: clsx(selected && 'bg-accent/20') + }; +} diff --git a/apps/landing/src/app/NavBar/index.tsx b/apps/landing/src/app/NavBar/index.tsx new file mode 100644 index 000000000..fdb4f2e10 --- /dev/null +++ b/apps/landing/src/app/NavBar/index.tsx @@ -0,0 +1,74 @@ +import { Discord, Github } from '@sd/assets/svgs/brands'; +import Image from 'next/image'; +import Link from 'next/link'; +import { PropsWithChildren } from 'react'; + +import { positions } from '../careers/data'; +import Logo from '../logo.png'; +import { MobileDropdown } from './MobileDropdown'; + +export function NavBar() { + return ( +
+
+ + Spacedrive logo +

Spacedrive

+ + +
+ Roadmap + Team + {/* Pricing */} + Blog + Docs +
+ Careers + {positions.length > 0 ? ( + + {` ${positions.length} `} + + ) : null} +
+
+
+ +
+ + + + + + +
+
+
+
+
+
+
+ ); +} + +function NavLink(props: PropsWithChildren<{ link?: string }>) { + return ( + + {props.children} + + ); +} diff --git a/apps/landing/src/app/NewBanner.tsx b/apps/landing/src/app/NewBanner.tsx new file mode 100644 index 000000000..c6516d0ed --- /dev/null +++ b/apps/landing/src/app/NewBanner.tsx @@ -0,0 +1,33 @@ +import { Newspaper } from '@phosphor-icons/react/dist/ssr'; +import clsx from 'clsx'; +import Link from 'next/link'; + +export interface NewBannerProps { + headline: string; + href: string; + link: string; + className?: string; +} + +export function NewBanner(props: NewBannerProps) { + const { headline, href, link } = props; + + return ( + +
+ +

{headline}

+
+
+ + {link} + + + ); +} diff --git a/apps/landing/src/app/Providers.tsx b/apps/landing/src/app/Providers.tsx new file mode 100644 index 000000000..93555f316 --- /dev/null +++ b/apps/landing/src/app/Providers.tsx @@ -0,0 +1,8 @@ +'use client'; + +import { PropsWithChildren } from 'react'; +import { TooltipProvider } from '@sd/ui'; + +export function Providers({ children }: PropsWithChildren) { + return {children}; +} diff --git a/apps/landing/src/app/blog/[slug]/page.tsx b/apps/landing/src/app/blog/[slug]/page.tsx new file mode 100644 index 000000000..00e63ba9f --- /dev/null +++ b/apps/landing/src/app/blog/[slug]/page.tsx @@ -0,0 +1,78 @@ +import { allPosts } from '@contentlayer/generated'; +import dayjs from 'dayjs'; +import { Metadata } from 'next'; +import { useMDXComponent } from 'next-contentlayer/hooks'; +import Image from 'next/image'; +import { notFound } from 'next/navigation'; +import { BlogTag } from '~/components/BlogTag'; +import { BlogMDXComponents } from '~/components/mdx'; + +export function generateStaticParams(): Array { + return allPosts.map((post) => ({ slug: post.slug })); +} + +interface Props { + params: { slug: string }; +} + +export function generateMetadata({ params }: Props): Metadata { + const post = allPosts.find((post) => post.slug === params.slug)!; + const description = + post.excerpt?.length || 0 > 160 ? post.excerpt?.substring(0, 160) + '...' : post.excerpt; + + return { + title: `${post.title} - Spacedrive Blog`, + description, + authors: { name: post.author }, + openGraph: { + title: post.title, + description, + images: post.image + }, + twitter: { + card: 'summary_large_image' + } + }; +} + +export default function Page({ params }: Props) { + const post = allPosts.find((post) => post.slug === params.slug); + + if (!post) notFound(); + + const MDXContent = useMDXComponent(post.body.code); + + return ( +
+ <> +
+ {post.imageAlt +
+
+
+

+ {post.title} +

+

+ by {post.author} · {dayjs(post.date).format('MM/DD/YYYY')} +

+
+
+ {post.tags.map((tag) => ( + + ))} +
+
+
+ +
+ +
+ ); +} diff --git a/apps/landing/src/app/blog/page.tsx b/apps/landing/src/app/blog/page.tsx new file mode 100644 index 000000000..ee67635f4 --- /dev/null +++ b/apps/landing/src/app/blog/page.tsx @@ -0,0 +1,54 @@ +import { allPosts } from '@contentlayer/generated'; +import dayjs from 'dayjs'; +import Image from 'next/image'; +import Link from 'next/link'; +import { BlogTag } from '~/components/BlogTag'; + +export const metadata = { + title: 'Spacedrive Blog', + description: 'Get the latest from Spacedrive.' +}; + +export default function Page() { + return ( +
+
+

Blog

+

Get the latest from Spacedrive.

+
+
+ {allPosts.map((post) => ( + + {post.image && ( + {post.imageAlt + )} +
+

{post.title}

+ {post.readTime} + {/*

{post.excerpt}

*/} +

+ by {post.author} · {dayjs(post.date).format('MM/DD/YYYY')} +

+
+ {post.tags.map((tag) => ( + + ))} +
+
+ + ))} +
+
+ ); +} diff --git a/apps/landing/src/app/careers/data.ts b/apps/landing/src/app/careers/data.ts new file mode 100644 index 000000000..6c9e33413 --- /dev/null +++ b/apps/landing/src/app/careers/data.ts @@ -0,0 +1,82 @@ +import { + Clock, + CurrencyDollar, + Desktop, + Heart, + House, + LightningSlash, + Smiley, + Star, + TrendUp +} from '@phosphor-icons/react/dist/ssr'; + +export interface PositionPosting { + name: string; + type: string; + salary: string; + description: string; +} + +export const positions: PositionPosting[] = []; + +export const values = [ + { + title: 'Async', + desc: 'To accommodate our international team and community, we work and communicate asynchronously.', + icon: Clock + }, + { + title: 'Quality', + desc: 'From our interface design to our code, we strive to build software that will last.', + icon: Star + }, + { + title: 'Speed', + desc: 'We get things done quickly, through small iteration cycles and frequent updates.', + icon: LightningSlash + }, + { + title: 'Transparency', + desc: 'We are human beings that make mistakes, but through total transparency we can solve them faster.', + icon: Heart + } +]; + +export const perks = [ + { + title: 'Competitive Salary', + desc: `We want the best, and will pay for the best. If you shine through we'll make sure you're paid what you're worth.`, + icon: CurrencyDollar, + color: '#0DD153' + }, + { + title: 'Stock Options', + desc: `As an early employee, you deserve to own a piece of our company. Stock options will be offered as part of your onboarding process.`, + icon: TrendUp, + color: '#BD0DD1' + }, + { + title: 'Paid Time Off', + desc: `Rest is important, you deliver your best work when you've had your downtime. We offer 4 weeks paid time off per year, and if you need more, we'll give you more.`, + icon: Smiley, + color: '#9210FF' + }, + { + title: 'Work From Home', + desc: `As an open source project, we're remote first and intend to keep it that way. Sorry Elon.`, + icon: House, + color: '#D1A20D' + }, + { + title: 'Desk Budget', + desc: `Need an M1 MacBook Pro? We've got you covered. (You'll probably need one with Rust compile times)`, + icon: Desktop, + color: '#0DC5D1' + }, + { + title: 'Health Care', + desc: `We use Deel for hiring and payroll, all your health care needs are covered.`, + icon: Heart, + color: '#D10D7F' + } +]; diff --git a/apps/landing/src/app/careers/page.tsx b/apps/landing/src/app/careers/page.tsx new file mode 100644 index 000000000..f99a3a015 --- /dev/null +++ b/apps/landing/src/app/careers/page.tsx @@ -0,0 +1,137 @@ +import { Clock, CurrencyDollar } from '@phosphor-icons/react/dist/ssr'; +import { Button } from '@sd/ui'; + +import { perks, positions, values } from './data'; + +export const metadata = { + title: 'Careers - Spacedrive', + description: 'Work with us to build the future of file management.' +}; + +export default function CareersPage() { + return ( +
+
+

+ Build the future of files. +

+
+

+ Spacedrive is redefining the way we think about our personal data, building a + open ecosystem to help preserve your digital legacy and make cross-platform file + management a breeze. +

+ +
+

+ Our Values +

+

What drives us daily.

+
+ {values.map((value, index) => ( +
+ +

+ {value.title} +

+

{value.desc}

+
+ ))} +
+
+

+ Perks and Benefits +

+

We're behind you 100%.

+
+ {perks.map((value, index) => ( +
+ +

{value.title}

+

{value.desc}

+
+ ))} +
+
+

+ Open Positions +

+ {positions.length === 0 ? ( +

+ There are no positions open at this time. Please check back later! +

+ ) : ( + <> +

If any open positions suit you, apply now!

+
+ {positions.map((value, index) => ( +
+
+

{value.name}

+
+ + + {value.salary} + + + + {value.type} + +
+
+

{value.description}

+
+ ))} +
+ + )} + +
+

+ How to apply? +

+

+ Send your cover letter and resume to{' '} + careers at spacedrive dot com and we'll get back to you + shortly! +

+
+
+ ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/Index.tsx b/apps/landing/src/app/docs/[[...slug]]/Index.tsx new file mode 100644 index 000000000..ce407b7e9 --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/Index.tsx @@ -0,0 +1,23 @@ +import Link from 'next/link'; + +import { Markdown } from './Markdown'; + +export function Index() { + return ( + +
+

Spacedrive Docs

+

+ Welcome to the Spacedrive documentation. Here you can find all the information + you need to get started with Spacedrive. +

+ + Get Started → + +
+
+ ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/Markdown.tsx b/apps/landing/src/app/docs/[[...slug]]/Markdown.tsx new file mode 100644 index 000000000..e724caa97 --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/Markdown.tsx @@ -0,0 +1,23 @@ +import clsx from 'clsx'; +import { PropsWithChildren } from 'react'; + +interface MarkdownPageProps { + classNames?: string; + articleClassNames?: string; +} + +export function Markdown(props: PropsWithChildren) { + return ( +
+
+ {props.children} +
+
+ ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/Search.tsx b/apps/landing/src/app/docs/[[...slug]]/Search.tsx new file mode 100644 index 000000000..90967d447 --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/Search.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { SearchInput } from '@sd/ui'; + +export function SearchBar() { + return ( +
alert('Search coming soon...')} className="mb-5"> + ⌘K} + /> +
+ ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/Sidebar.tsx b/apps/landing/src/app/docs/[[...slug]]/Sidebar.tsx new file mode 100644 index 000000000..2e2ad9f80 --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/Sidebar.tsx @@ -0,0 +1,81 @@ +import clsx from 'clsx'; +import Link from 'next/link'; +import { iconConfig } from '~/utils/contentlayer'; +import { toTitleCase } from '~/utils/util'; + +import { navigationMeta } from './data'; +import { SearchBar } from './Search'; + +interface Props { + slug?: string[]; +} + +export function Sidebar({ slug }: Props) { + const slugString = slug?.join('/') ?? '/'; + const currentSection = + navigationMeta.find((section) => section.slug === slug?.[0]) ?? navigationMeta[0]; + + return ( + + ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/data.ts b/apps/landing/src/app/docs/[[...slug]]/data.ts new file mode 100644 index 000000000..154a0fa1e --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/data.ts @@ -0,0 +1,47 @@ +import { allDocs } from '@contentlayer/generated'; +import { getDocsNavigation } from '~/utils/contentlayer'; + +const navigation = getDocsNavigation(allDocs); + +export function getDoc(params: string[]) { + const slug = params.join('/'); + + const doc = allDocs.find((doc) => doc.slug === slug); + + if (!doc) { + return { + notFound: true + }; + } + + const docNavigation = getDocsNavigation(allDocs); + + // TODO: Doesn't work properly (can't skip categories) + const docIndex = docNavigation + .find((sec) => sec.slug == doc.section) + ?.categories.find((cat) => cat.slug == doc.category) + ?.docs.findIndex((d) => d.slug == doc.slug); + + const nextDoc = + docNavigation + .find((sec) => sec.slug == doc.section) + ?.categories.find((cat) => cat.slug == doc.category)?.docs[(docIndex || 0) + 1] || null; + + return { + navigation: docNavigation, + doc, + nextDoc + }; +} + +export const navigationMeta = navigation.map((section) => ({ + slug: section.slug, + categories: section.categories.map((category) => ({ + ...category, + docs: category.docs.map((doc) => ({ + url: doc.url, + slug: doc.slug, + title: doc.title + })) + })) +})); diff --git a/apps/landing/src/app/docs/[[...slug]]/layout.tsx b/apps/landing/src/app/docs/[[...slug]]/layout.tsx new file mode 100644 index 000000000..fca2a23eb --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/layout.tsx @@ -0,0 +1,67 @@ +'use client'; + +import { CaretRight, List, X } from '@phosphor-icons/react'; +import { PropsWithChildren, useState } from 'react'; +import { slide as Menu } from 'react-burger-menu'; +import { Button } from '@sd/ui'; + +import { Sidebar } from './Sidebar'; + +import 'katex/dist/katex.min.css'; + +import { toTitleCase } from '~/utils/util'; + +export default function Layout({ + children, + params +}: PropsWithChildren<{ params: { slug?: string[] } }>) { + const [menuOpen, setMenuOpen] = useState(false); + + return ( +
+ setMenuOpen(false)} + customBurgerIcon={false} + isOpen={menuOpen} + pageWrapId="page-container" + className="shadow-2xl shadow-black" + > +
+ + +
+
+ +
+
+
+ +
+ {params.slug?.map((item, index) => { + if (index === 2) return null; + return ( +
+ {toTitleCase(item)} + {index < 1 && } +
+ ); + })} +
+
{children}
+
+
+
+ ); +} diff --git a/apps/landing/src/app/docs/[[...slug]]/page.tsx b/apps/landing/src/app/docs/[[...slug]]/page.tsx new file mode 100644 index 000000000..fcf3a88f3 --- /dev/null +++ b/apps/landing/src/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,81 @@ +import { allDocs } from '@contentlayer/generated'; +import { CaretRight } from '@phosphor-icons/react/dist/ssr'; +import { Github } from '@sd/assets/svgs/brands'; +import { Metadata } from 'next'; +import { getMDXComponent } from 'next-contentlayer/hooks'; +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { DocMDXComponents } from '~/components/mdx'; +import { toTitleCase } from '~/utils/util'; + +import { getDoc } from './data'; +import { Index } from './Index'; +import { Markdown } from './Markdown'; + +export function generateStaticParams() { + const slugs = allDocs.map((doc) => doc.slug); + return slugs.map((slug) => ({ slug: slug.split('/') })); +} + +interface Props { + params: { slug?: string[] }; +} + +export function generateMetadata({ params }: Props): Metadata { + if (!params.slug) + return { + title: 'Spacedrive Docs', + description: 'Learn more about Spacedrive' + }; + + return {}; +} + +export default function Page({ params }: Props) { + if (!params.slug) return ; + + const { doc, nextDoc } = getDoc(params.slug); + + if (!doc) notFound(); + + const MDXContent = getMDXComponent(doc.body.code); + + return ( + +
+ {toTitleCase(doc.category)} +
+ +
+ + + + Edit this page on GitHub + + + {nextDoc && ( + + + + Next article: {nextDoc.title} + + + )} +
+
+ ); +} + +function BottomCard(props: any) { + return ( +
+ ); +} diff --git a/apps/landing/public/favicon.ico b/apps/landing/src/app/favicon.ico similarity index 100% rename from apps/landing/public/favicon.ico rename to apps/landing/src/app/favicon.ico diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx new file mode 100644 index 000000000..5db9ec027 --- /dev/null +++ b/apps/landing/src/app/layout.tsx @@ -0,0 +1,41 @@ +import { PropsWithChildren } from 'react'; + +import { Footer } from './Footer'; +import { NavBar } from './NavBar'; + +import '@sd/ui/style/style.scss'; +import '~/styles/prism.css'; +import '~/styles/style.scss'; + +import { Providers } from './Providers'; + +export const metadata = { + themeColor: { color: '#E751ED', media: 'not screen' }, + robots: 'index, follow', + description: + 'Combine your drives and clouds into one database that you can organize and explore from any device. Designed for creators, hoarders and the painfully disorganized.', + openGraph: { + images: 'https://spacedrive.com/logo.png' + }, + keywords: + 'files,file manager,spacedrive,file explorer,vdfs,distributed filesystem,cas,content addressable storage,virtual filesystem,photos app, video organizer,video encoder,tags,tag based filesystem', + authors: { name: 'Spacedrive Technology Inc.', url: 'https://spacedrive.com' } +}; + +export default function Layout({ children }: PropsWithChildren) { + return ( + + + +
+ +
+ {children} +
+
+
+
+ + + ); +} diff --git a/apps/landing/src/app/logo.png b/apps/landing/src/app/logo.png new file mode 100644 index 000000000..d84e109ee Binary files /dev/null and b/apps/landing/src/app/logo.png differ diff --git a/apps/landing/src/app/not-found.tsx b/apps/landing/src/app/not-found.tsx new file mode 100644 index 000000000..fab036b09 --- /dev/null +++ b/apps/landing/src/app/not-found.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { SmileyXEyes } from '@phosphor-icons/react/dist/ssr'; +import { useRouter } from 'next/navigation'; +import { Button } from '@sd/ui'; +import Markdown from '~/components/Markdown'; + +export const metadata = { + title: 'Not Found - Spacedrive' +}; + +export default function NotFound() { + const router = useRouter(); + + return ( + +
+
+ +

+ In the quantum realm this page potentially exists. +

+

In other words, thats a 404.

+
+ + +
+
+
+ + ); +} diff --git a/apps/landing/src/app/page.tsx b/apps/landing/src/app/page.tsx new file mode 100644 index 000000000..ee8e4c8e8 --- /dev/null +++ b/apps/landing/src/app/page.tsx @@ -0,0 +1,110 @@ +import Image from 'next/image'; +import CyclingImage from '~/components/CyclingImage'; + +import { Background } from './Background'; +import { Downloads } from './Downloads'; +import { NewBanner } from './NewBanner'; + +export const metadata = { + title: 'Spacedrive — A file manager from the future.', + description: + 'Combine your drives and clouds into one database that you can organize and explore from any device. Designed for creators, hoarders and the painfully disorganized.', + openGraph: { + images: 'https://raw.githubusercontent.com/spacedriveapp/.github/main/profile/spacedrive_icon.png' + }, + keywords: + 'files,file manager,spacedrive,file explorer,vdfs,distributed filesystem,cas,content addressable storage,virtual filesystem,photos app, video organizer,video encoder,tags,tag based filesystem', + authors: { + name: 'Spacedrive Technology Inc.', + url: 'https://spacedrive.com' + } +}; + +export default function Page() { + return ( + <> + + l +
+