Migrate landing page to app dir (#1587)

* app dir yay

* better docs route

* fix icon sizing

* mobile sidebars

* detect webgl in useEffect

* separate zxcvbn
This commit is contained in:
Brendan Allan 2023-10-16 12:28:16 +08:00 committed by GitHub
parent f352a28dc8
commit d4ad5c97f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2243 additions and 2089 deletions

View file

@ -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';

View file

@ -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;

View file

@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -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));

View file

@ -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"

View file

@ -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<ReactNode>(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<typeof setTimeout>;
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() ? <Space /> : <Bubbles />);
}, []);
return (
<div style={{ opacity }}>
<Suspense>{!isWindowResizing && inner}</Suspense>
</div>
);
}

View file

@ -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<any>;
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<string, Platform>;
const BASE_DL_LINK = '/api/releases/desktop/stable';
export function Downloads() {
const [selectedPlatform, setSelectedPlatform] = useState<Platform | null>(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 (
<>
<div className="flex flex-row gap-3">
{currentPlatform &&
(() => {
const Icon = currentPlatform.icon;
const { links } = currentPlatform;
return (
<HomeCTA
href={
links?.length === 1
? `${BASE_DL_LINK}/${currentPlatform.os}/${links[0].arch}`
: undefined
}
className={`z-5 plausible-event-name=download relative plausible-event-os=${currentPlatform.name}`}
icon={Icon ? <Icon width="1rem" height="1rem" /> : undefined}
text={`Download for ${currentPlatform.name}`}
onClick={() => setSelectedPlatform(currentPlatform)}
/>
);
})()}
<HomeCTA
target="_blank"
href="https://www.github.com/spacedriveapp/spacedrive"
icon={<Github />}
className="z-5 relative"
text="Star on GitHub"
/>
</div>
{selectedPlatform?.links && selectedPlatform.links.length > 1 && (
<div className="z-50 mb-2 mt-4 flex flex-row gap-3 fade-in">
{selectedPlatform.links.map(({ name, arch }) => (
<HomeCTA
key={name}
size="md"
text={name}
target="_blank"
href={`${BASE_DL_LINK}/${selectedPlatform.os}/${arch}`}
className={clsx(
'z-5 relative !py-1 !text-sm',
`plausible-event-name=download plausible-event-os=${selectedPlatform.name}+${arch}`
)}
/>
))}
</div>
)}
<p className="animation-delay-3 z-30 mt-3 px-6 text-center text-sm text-gray-400 fade-in">
{RELEASE_VERSION}
{formattedVersion && (
<>
<span className="mx-2 opacity-50">|</span>
{formattedVersion}
</>
)}
</p>
<div className="relative z-10 mt-5 flex gap-3">
{Object.values(platforms as Record<string, Platform>).map((platform, i) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.2, ease: 'easeInOut' }}
key={platform.name}
>
<Platform
key={platform.name}
platform={platform}
className={clsx(
platform.links?.length == 1 &&
`plausible-event-name=download plausible-event-os=${platform.name}`
)}
onClick={() => {
if (platform.links && platform.links.length > 1)
setSelectedPlatform(platform);
}}
/>
</motion.div>
);
})}
</div>
</>
);
}
function useCurrentPlatform() {
const [currentPlatform, setCurrentPlatform] = useState<Platform | null>(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) => (
<a
aria-label={platform.name}
target="_blank"
href={`${BASE_DL_LINK}/${platform.os}/${links[0].arch}`}
{...props}
/>
)
: (props: any) => <button {...props} />
: (props: any) => <div {...props} />;
const Icon = platform.icon;
return (
<Tooltip label={platform.name}>
<Outer {...props}>
<Icon
size={25}
className={`h-[25px] text-white ${
platform.links ? 'opacity-80' : 'opacity-20'
}`}
weight="fill"
/>
</Outer>
</Tooltip>
);
}

View file

@ -0,0 +1,158 @@
import {
Discord,
Github,
Instagram,
Opencollective,
Twitch,
Twitter
} from '@sd/assets/svgs/brands';
import Image from 'next/image';
import Link from 'next/link';
import { PropsWithChildren } from 'react';
import Logo from './logo.png';
export function Footer() {
return (
<footer id="footer" className="relative z-50 w-screen overflow-hidden pt-3 backdrop-blur">
<Image
alt="footer gradient"
className="absolute bottom-0 left-0 z-[-1]"
quality={100}
width={0}
height={0}
src="/images/footergradient.webp"
style={{ width: '100%', height: '400px' }}
sizes="100vw"
/>
<div className="min-h-64 m-auto grid max-w-[100rem] grid-cols-2 gap-6 p-8 pb-20 pt-10 text-white sm:grid-cols-2 lg:grid-cols-6">
<div className="col-span-2">
<Image alt="Spacedrive logo" src={Logo} className="mb-5 h-10 w-10" />
<h1 className="mb-1 text-xl font-bold">Spacedrive</h1>
<p className="text-sm text-gray-350 opacity-50">
&copy; Copyright {new Date().getFullYear()} Spacedrive Technology Inc.
</p>
<div className="mb-10 mt-12 flex flex-row space-x-3">
<FooterLink link="https://x.com/spacedriveapp">
<Twitter className="h-6 w-6" />
</FooterLink>
<FooterLink aria-label="discord" link="https://discord.gg/gTaF2Z44f5">
<Discord className="h-6 w-6" />
</FooterLink>
<FooterLink
aria-label="instagram"
link="https://instagram.com/spacedriveapp"
>
<Instagram className="h-6 w-6" />
</FooterLink>
<FooterLink aria-label="github" link="https://github.com/spacedriveapp">
<Github className="h-6 w-6" />
</FooterLink>
<FooterLink
aria-label="open collective"
link="https://opencollective.com/spacedrive"
>
<Opencollective className="h-6 w-6" />
</FooterLink>
<FooterLink
aria-label="twitch stream"
link="https://twitch.tv/jamiepinelive"
>
<Twitch className="h-6 w-6" />
</FooterLink>
</div>
</div>
<div className="col-span-1 flex flex-col space-y-2">
<h1 className="mb-1 text-xs font-bold uppercase ">About</h1>
<FooterLink link="/team">Team</FooterLink>
<FooterLink link="/docs/product/resources/faq">FAQ</FooterLink>
<FooterLink link="/careers">Careers</FooterLink>
<FooterLink link="/docs/changelog/beta/0.1.0">Changelog</FooterLink>
<FooterLink link="/blog">Blog</FooterLink>
</div>
<div className="col-span-1 flex flex-col space-y-2">
<h1 className="mb-1 text-xs font-bold uppercase">Downloads</h1>
<div className="col-span-1 flex flex-col space-y-2">
<FooterLink link="https://spacedrive.com/api/releases/desktop/stable/darwin/aarch64">
macOS
</FooterLink>
<FooterLink link="https://spacedrive.com/api/releases/desktop/stable/darwin/x86_64">
macOS Intel
</FooterLink>
<FooterLink link="https://spacedrive.com/api/releases/desktop/stable/windows/x86_64">
Windows
</FooterLink>
<FooterLink link="https://spacedrive.com/api/releases/desktop/stable/linux/x86_64">
Linux
</FooterLink>
</div>
<div className="pointer-events-none col-span-1 flex flex-col space-y-2 opacity-50">
<FooterLink link="#">Android</FooterLink>
<FooterLink link="#">iOS</FooterLink>
</div>
</div>
<div className="col-span-1 flex flex-col space-y-2">
<h1 className="mb-1 text-xs font-bold uppercase ">Developers</h1>
<FooterLink link="/docs/product/getting-started/introduction">
Documentation
</FooterLink>
<FooterLink
blank
link="https://github.com/spacedriveapp/spacedrive/blob/main/CONTRIBUTING.md"
>
Contribute
</FooterLink>
<div className="pointer-events-none opacity-50">
<FooterLink link="#">Extensions</FooterLink>
</div>
<div className="pointer-events-none opacity-50">
<FooterLink link="#">Self Host</FooterLink>
</div>
</div>
<div className="col-span-1 flex flex-col space-y-2">
<h1 className="mb-1 text-xs font-bold uppercase ">Org</h1>
<FooterLink blank link="https://opencollective.com/spacedrive">
Open Collective
</FooterLink>
<FooterLink
blank
link="https://github.com/spacedriveapp/spacedrive/blob/main/LICENSE"
>
License
</FooterLink>
<div>
<FooterLink link="/docs/company/legal/privacy">Privacy</FooterLink>
</div>
<div>
<FooterLink link="/docs/company/legal/terms">Terms</FooterLink>
</div>
</div>
</div>
<div className="absolute top-0 flex h-1 w-full flex-row items-center justify-center opacity-100">
<div className="h-[1px] w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-[1px] w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
</div>
</footer>
);
}
function FooterLink({
blank,
link,
...props
}: PropsWithChildren<{ link: string; blank?: boolean }>) {
return (
<Link
href={link}
target={blank ? '_blank' : ''}
className="text-gray-300 duration-300 hover:text-white hover:opacity-50"
rel="noreferrer"
{...props}
>
{props.children}
</Link>
);
}

View file

@ -0,0 +1,32 @@
'use client';
import { type IconProps } from '@phosphor-icons/react';
import clsx from 'clsx';
import { Button, LinkButtonProps } from '@sd/ui';
interface Props extends LinkButtonProps {
className?: string;
text: string;
icon?: IconProps;
onClick?: () => void;
}
export function HomeCTA({ className, text, icon, ...props }: Props) {
return (
<Button
size="lg"
className={clsx(
'home-button-border-gradient relative z-30 flex cursor-pointer items-center gap-2 !rounded-[7px] border-0 !bg-[#2F3152]/30 py-2 text-sm text-white !backdrop-blur-lg hover:brightness-110 md:text-[16px]',
className
)}
{...props}
>
<>
{icon && icon}
{text}
</>
</Button>
);
}
export default HomeCTA;

View file

@ -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 (
<Dropdown.Root
button={
<Button
aria-label="mobile-menu"
className="ml-[140px] hover:!bg-transparent"
size="icon"
>
<DotsThreeVertical weight="bold" className="h-6 w-6 " />
</Button>
}
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]"
>
<Dropdown.Section>
<a href="https://discord.gg/gTaF2Z44f5" target="_blank">
<Dropdown.Item icon={Discord}>Join Discord</Dropdown.Item>
</a>
<a href="https://github.com/spacedriveapp/spacedrive" target="_blank">
<Dropdown.Item icon={Github}>Repository</Dropdown.Item>
</a>
</Dropdown.Section>
<Dropdown.Section>
<Dropdown.Item icon={MapPin} {...link('/roadmap')}>
Roadmap
</Dropdown.Item>
<Dropdown.Item icon={User} {...link('/team')}>
Team
</Dropdown.Item>
{/* <Dropdown.Item icon={Money} {...link('/pricing', router)}>
Pricing
</Dropdown.Item> */}
<Dropdown.Item icon={Chat} {...link('/blog')}>
Blog
</Dropdown.Item>
<Dropdown.Item icon={Book} {...link('/docs/product/getting-started/introduction')}>
Docs
</Dropdown.Item>
<Dropdown.Item icon={Academia} {...link('/careers')}>
Careers
{positions.length > 0 ? (
<span className="ml-2 rounded-md bg-primary px-[5px] py-px text-xs">
{positions.length}
</span>
) : null}
</Dropdown.Item>
</Dropdown.Section>
</Dropdown.Root>
);
}
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')
};
}

View file

@ -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 (
<div className="navbar-blur fixed z-[55] h-16 w-full !bg-black/10 px-2 transition">
<div className="relative m-auto flex h-full max-w-[100rem] items-center p-5">
<Link href="/" className="absolute flex flex-row items-center">
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 h-8 w-8" />
<h3 className="text-xl font-bold text-white">Spacedrive</h3>
</Link>
<div className="m-auto hidden space-x-4 text-white lg:block ">
<NavLink link="/roadmap">Roadmap</NavLink>
<NavLink link="/team">Team</NavLink>
{/* <NavLink link="/pricing">Pricing</NavLink> */}
<NavLink link="/blog">Blog</NavLink>
<NavLink link="/docs/product/getting-started/introduction">Docs</NavLink>
<div className="relative inline">
<NavLink link="/careers">Careers</NavLink>
{positions.length > 0 ? (
<span className="absolute -right-2 -top-1 rounded-md bg-primary/80 px-[5px] text-xs">
{` ${positions.length} `}
</span>
) : null}
</div>
</div>
<div className="flex-1 lg:hidden" />
<MobileDropdown />
<div className="absolute right-3 hidden flex-row space-x-5 lg:flex">
<Link
aria-label="discord"
href="https://discord.gg/gTaF2Z44f5"
target="_blank"
rel="noreferrer"
>
<Discord className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
<Link
aria-label="github"
href="https://github.com/spacedriveapp/spacedrive"
target="_blank"
rel="noreferrer"
>
<Github className="h-6 w-6 text-white opacity-100 duration-300 hover:opacity-50" />
</Link>
</div>
</div>
<div className="absolute bottom-0 flex h-1 w-full flex-row items-center justify-center pt-4 opacity-100">
<div className="h-[1px] w-1/2 bg-gradient-to-r from-transparent to-white/10"></div>
<div className="h-[1px] w-1/2 bg-gradient-to-l from-transparent to-white/10"></div>
</div>
</div>
);
}
function NavLink(props: PropsWithChildren<{ link?: string }>) {
return (
<Link
href={props.link ?? '#'}
target={props.link?.startsWith('http') ? '_blank' : undefined}
className="cursor-pointer p-4 text-[11pt] text-gray-300 no-underline transition hover:text-gray-50"
rel="noreferrer"
>
{props.children}
</Link>
);
}

View file

@ -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 (
<Link
href={href}
className={clsx(
props.className,
'news-banner-border-gradient news-banner-glow animation-delay-1 fade-in-whats-new z-10 mb-5 flex w-fit flex-row rounded-full bg-black/10 px-5 py-2.5 text-xs backdrop-blur-md transition hover:bg-purple-900/20 sm:w-auto sm:text-base'
)}
>
<div className="flex items-center gap-2">
<Newspaper weight="fill" className="text-white " size={20} />
<p className="font-regular truncate text-white">{headline}</p>
</div>
<div role="separator" className="h-22 mx-4 w-[1px] bg-zinc-700/70" />
<span className="font-regular shrink-0 bg-gradient-to-r from-violet-400 to-fuchsia-400 bg-clip-text text-transparent decoration-primary-600">
{link} <span aria-hidden="true">&rarr;</span>
</span>
</Link>
);
}

View file

@ -0,0 +1,8 @@
'use client';
import { PropsWithChildren } from 'react';
import { TooltipProvider } from '@sd/ui';
export function Providers({ children }: PropsWithChildren) {
return <TooltipProvider>{children}</TooltipProvider>;
}

View file

@ -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<Props['params']> {
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 (
<div className="lg:prose-xs prose dark:prose-invert container m-auto mb-20 max-w-4xl p-4 pt-14">
<>
<figure>
<Image
src={post.image}
alt={post.imageAlt ?? ''}
className="mt-8 rounded-xl"
height={400}
width={900}
/>
</figure>
<section className="-mx-8 flex flex-wrap gap-4 rounded-xl px-8">
<div className="w-full grow">
<h1 className="m-0 text-2xl leading-snug sm:text-4xl sm:leading-normal">
{post.title}
</h1>
<p className="m-0 mt-2">
by <b>{post.author}</b> &middot; {dayjs(post.date).format('MM/DD/YYYY')}
</p>
</div>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<BlogTag key={tag} name={tag} />
))}
</div>
</section>
<article id="content" className="text-lg">
<MDXContent components={BlogMDXComponents} />
</article>
</>
</div>
);
}

View file

@ -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 (
<div className="lg:prose-xs prose dark:prose-invert prose-a:no-underline container m-auto mb-20 flex max-w-4xl flex-col p-4 pt-32">
<section>
<h1 className="fade-in-heading m-0">Blog</h1>
<p className="fade-in-heading animation-delay-1">Get the latest from Spacedrive.</p>
</section>
<section className="animation-delay-2 mt-8 grid grid-cols-1 will-change-transform fade-in sm:grid-cols-1 lg:grid-cols-1">
{allPosts.map((post) => (
<Link
key={post.slug}
href={post.url}
className="relative z-0 mb-10 flex cursor-pointer flex-col gap-2 overflow-hidden rounded-xl border border-gray-500 transition-colors"
>
{post.image && (
<Image
src={post.image}
alt={post.imageAlt ?? ''}
className="inset-0 -z-10 m-0 w-full rounded-t-xl object-cover md:h-96"
// NOTE: Ideally we need to follow this specific ratio for our blog images
height={400}
width={800}
/>
)}
<div className="p-8">
<h2 className="text2xl m-0 md:text-4xl">{post.title}</h2>
<small className="m-0">{post.readTime}</small>
{/* <p className="line-clamp-3 my-2">{post.excerpt}</p> */}
<p className="m-0 text-white">
by {post.author} &middot; {dayjs(post.date).format('MM/DD/YYYY')}
</p>
<div className="mt-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<BlogTag key={tag} name={tag} />
))}
</div>
</div>
</Link>
))}
</section>
</div>
);
}

View file

@ -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'
}
];

View file

@ -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 (
<div className="prose prose-invert container relative m-auto mb-20 min-h-screen max-w-4xl p-4 pt-32 text-white">
<div
className="bloom subtle egg-bloom-two -top-60 right-[-400px]"
style={{ transform: 'scale(2)' }}
/>
<h1 className="fade-in-heading mb-3 px-2 text-center text-4xl font-black leading-tight text-white md:text-5xl">
Build the future of files.
</h1>
<div className="animation-delay-1 z-30 flex flex-col items-center fade-in">
<p className="z-40 text-center text-lg text-gray-350">
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.
</p>
<Button
href="#open-positions"
className="z-30 cursor-pointer border-0"
variant="accent"
>
See Open Positions
</Button>
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-4xl font-black leading-tight">
Our Values
</h2>
<p className="mb-4 mt-2">What drives us daily.</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
{values.map((value, index) => (
<div
key={value.title + index}
className="flex flex-col rounded-md border border-gray-500 bg-gray-550/50 p-10"
>
<value.icon
width="1em"
height="1em"
className="text-[32px]"
weight="bold"
/>
<h3 className="mb-1 mt-4 text-2xl font-bold leading-snug">
{value.title}
</h3>
<p className="mb-0 mt-1 text-gray-350">{value.desc}</p>
</div>
))}
</div>
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-4xl font-black leading-tight text-white">
Perks and Benefits
</h2>
<p className="mb-4 mt-2">We're behind you 100%.</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4 sm:grid-cols-3">
{perks.map((value, index) => (
<div
key={value.title + index}
style={{
backgroundColor: value.color + '10',
borderColor: value.color + '30'
}}
className="flex flex-col rounded-md border bg-gray-550/30 p-8"
>
<value.icon
width="1em"
height="1em"
className="text-[32px]"
weight="bold"
color={value.color}
/>
<h3 className="mb-1 mt-4">{value.title}</h3>
<p className="mb-0 mt-1 text-sm text-white opacity-60">{value.desc}</p>
</div>
))}
</div>
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2
id="open-positions"
className="mb-0 px-2 text-center text-4xl font-black leading-tight text-white"
>
Open Positions
</h2>
{positions.length === 0 ? (
<p className="mt-2 text-center text-gray-350">
There are no positions open at this time. Please check back later!
</p>
) : (
<>
<p className="mb-4 mt-2">If any open positions suit you, apply now!</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4">
{positions.map((value, index) => (
<div
key={value.name + index}
className="flex flex-col rounded-md border border-gray-500 bg-gray-550/50 p-10"
>
<div className="flex flex-col sm:flex-row">
<h3 className="m-0 text-2xl leading-tight">{value.name}</h3>
<div className="mt-3 sm:mt-0.5">
<span className="text-sm font-semibold text-gray-300 sm:ml-4">
<CurrencyDollar className="-mt-1 mr-1 inline w-4" />
{value.salary}
</span>
<span className="ml-4 text-sm font-semibold text-gray-300">
<Clock className="-mt-1 mr-1 inline w-4" />
{value.type}
</span>
</div>
</div>
<p className="mb-0 mt-3 text-gray-350">{value.description}</p>
</div>
))}
</div>
</>
)}
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-3xl font-black text-white">
How to apply?
</h2>
<p className="mt-2">
Send your cover letter and resume to{' '}
<strong>careers at spacedrive dot com</strong> and we'll get back to you
shortly!
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,23 @@
import Link from 'next/link';
import { Markdown } from './Markdown';
export function Index() {
return (
<Markdown>
<div className="mt-[105px]">
<h1 className="text-4xl font-bold">Spacedrive Docs</h1>
<p className="text-lg text-gray-400">
Welcome to the Spacedrive documentation. Here you can find all the information
you need to get started with Spacedrive.
</p>
<Link
className="text-primary-600 transition hover:text-primary-500"
href="/docs/product/getting-started/introduction"
>
Get Started
</Link>
</div>
</Markdown>
);
}

View file

@ -0,0 +1,23 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
interface MarkdownPageProps {
classNames?: string;
articleClassNames?: string;
}
export function Markdown(props: PropsWithChildren<MarkdownPageProps>) {
return (
<div className={clsx('mb-10 p-4', props.classNames)}>
<article
id="content"
className={clsx(
'lg:prose-xs prose prose-h1:text-[3.25em] prose-a:text-primary prose-a:no-underline prose-blockquote:rounded prose-blockquote:bg-gray-600 prose-code:rounded-md prose-code:bg-gray-650 prose-code:p-1 prose-code:font-normal prose-code:text-gray-400 prose-code:before:hidden prose-code:after:hidden prose-table:border-b prose-table:border-gray-500 prose-tr:even:bg-gray-700 prose-th:p-2 prose-td:border-l prose-td:border-gray-500 prose-td:p-2 prose-td:last:border-r prose-img:rounded dark:prose-invert text-[15px] sm:text-[16px]',
props.articleClassNames
)}
>
{props.children}
</article>
</div>
);
}

View file

@ -0,0 +1,15 @@
'use client';
import { SearchInput } from '@sd/ui';
export function SearchBar() {
return (
<div onClick={() => alert('Search coming soon...')} className="mb-5">
<SearchInput
placeholder="Search..."
disabled
right={<span className="pr-2 text-xs font-semibold text-gray-400">K</span>}
/>
</div>
);
}

View file

@ -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 (
<nav className="mr-8 flex w-full flex-col sm:w-52">
<SearchBar />
<div className="mb-6 flex flex-col">
{navigationMeta.map((section) => {
const isActive = section.slug === currentSection.slug;
const Icon = iconConfig[section.slug];
return (
<Link
// Use the first page in the section as the link
href={section.categories[0]?.docs[0]?.url}
key={section.slug}
className={clsx(
`doc-sidebar-button flex items-center py-1.5 text-[14px] font-semibold`,
section.slug,
isActive && 'nav-active'
)}
>
<div
className={clsx(
`mr-4 rounded-lg border-t border-gray-400/20 bg-gray-500 p-1`
)}
>
<Icon weight="bold" className="h-4 w-4 text-white opacity-80" />
</div>
{toTitleCase(section.slug)}
</Link>
);
})}
</div>
{currentSection?.categories.map((category) => (
<div className="mb-5" key={category.title}>
<h2 className="font-semibold no-underline">{category.title}</h2>
<ul className="mt-3">
{category.docs.map((doc) => {
const active = slugString === doc.slug;
return (
<li
className={clsx(
'flex border-l border-gray-600',
active && 'border-l-2 border-primary'
)}
key={doc.title}
>
<Link
href={doc.url}
className={clsx(
'w-full rounded px-3 py-1 text-[14px] font-normal text-gray-350 no-underline hover:text-gray-50',
active && '!font-medium !text-white '
)}
>
{doc.title}
</Link>
{/* this fixes the links no joke */}
{active && <div />}
</li>
);
})}
</ul>
</div>
))}
</nav>
);
}

View file

@ -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
}))
}))
}));

View file

@ -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 (
<div className="flex w-full flex-col items-start sm:flex-row">
<Menu
onClose={() => setMenuOpen(false)}
customBurgerIcon={false}
isOpen={menuOpen}
pageWrapId="page-container"
className="shadow-2xl shadow-black"
>
<div className="custom-scroll doc-sidebar-scroll visible h-screen overflow-x-hidden bg-gray-650 px-7 pb-20 pt-7 sm:invisible">
<Button
onClick={() => setMenuOpen(!menuOpen)}
className="-ml-0.5 mb-3 !border-none !px-1"
>
<X weight="bold" className="h-6 w-6" />
</Button>
<Sidebar slug={params.slug} />
</div>
</Menu>
<aside className="sticky top-32 mb-20 ml-2 mr-0 mt-32 hidden px-5 sm:inline lg:mr-4">
<Sidebar slug={params.slug} />
</aside>
<div className="flex w-full flex-col sm:flex-row" id="page-container">
<div className="mt-[65px] flex h-12 w-full items-center border-y border-gray-600 px-5 sm:hidden">
<div className="flex sm:hidden">
<Button
className="ml-1 !border-none !px-2"
onClick={() => setMenuOpen(!menuOpen)}
>
<List weight="bold" className="h-6 w-6" />
</Button>
</div>
{params.slug?.map((item, index) => {
if (index === 2) return null;
return (
<div key={index} className="ml-2 flex flex-row items-center">
<span className="px-1 text-sm">{toTitleCase(item)}</span>
{index < 1 && <CaretRight className="-mr-2 ml-1 h-4 w-4" />}
</div>
);
})}
</div>
<div className="mx-4 overflow-x-hidden sm:mx-auto">{children}</div>
<div className="w-0 sm:w-32 lg:w-64" />
</div>
</div>
);
}

View file

@ -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 <Index />;
const { doc, nextDoc } = getDoc(params.slug);
if (!doc) notFound();
const MDXContent = getMDXComponent(doc.body.code);
return (
<Markdown classNames="sm:mt-[105px] mt-6 min-h-screen ">
<h5 className="mb-2 text-sm font-semibold text-primary lg:min-w-[700px]">
{toTitleCase(doc.category)}
</h5>
<MDXContent components={DocMDXComponents} />
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
<Link
target="_blank"
rel="noreferrer"
href={`https://github.com/spacedriveapp/spacedrive/blob/main${doc.url}.mdx`}
className="w-full"
>
<BottomCard>
<Github className="mr-3 w-5" />
Edit this page on GitHub
</BottomCard>
</Link>
{nextDoc && (
<Link href={nextDoc.url} className="w-full">
<BottomCard>
<CaretRight className="mr-3 w-5" />
Next article: {nextDoc.title}
</BottomCard>
</Link>
)}
</div>
</Markdown>
);
}
function BottomCard(props: any) {
return (
<div
className="group flex flex-row items-center rounded-lg border border-gray-700 p-4 text-sm !text-gray-200 transition-all duration-200 hover:translate-y-[-2px] hover:border-primary hover:!text-primary hover:shadow-xl hover:shadow-primary/10"
{...props}
/>
);
}

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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 (
<html lang="en" className="dark scroll-smooth">
<body>
<Providers>
<div className="overflow-hidden dark:bg-[#030014]/60">
<NavBar />
<main className="dark z-10 m-auto max-w-[100rem] dark:text-white">
{children}
</main>
<Footer />
</div>
</Providers>
</body>
</html>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

View file

@ -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 (
<Markdown classNames="flex w-full justify-center">
<div className="m-auto flex flex-col items-center ">
<div className="h-32" />
<SmileyXEyes className="mb-3 h-44 w-44" />
<h1 className="mb-2 text-center">
In the quantum realm this page potentially exists.
</h1>
<p>In other words, thats a 404.</p>
<div className="flex flex-wrap justify-center">
<Button
className="mr-3 mt-2 cursor-pointer "
variant="gray"
onClick={() => router.back()}
>
Back
</Button>
<Button href="/" className="mt-2 cursor-pointer !text-white" variant="accent">
Discover Spacedrive
</Button>
</div>
</div>
<div className="h-80" />
</Markdown>
);
}

View file

@ -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 (
<>
<Background />
<Image
loading="eager"
className="absolute-horizontal-center fade-in"
width={1278}
height={626}
alt="l"
src="/images/headergradient.webp"
/>
<div className="flex w-full flex-col items-center px-4">
<div className="mt-22 lg:mt-28" id="content" aria-hidden="true" />
<div className="mt-24 lg:mt-8" />
<NewBanner
headline="Alpha release is finally here!"
href="/blog/october-alpha-release"
link="Read post"
className="mt-[50px] lg:mt-0"
/>
<h1 className="fade-in-heading z-30 mb-3 bg-clip-text px-2 text-center text-4xl font-bold leading-tight text-white md:text-5xl lg:text-7xl">
One Explorer. All Your Files.
</h1>
<p className="animation-delay-1 fade-in-heading text-md leading-2 z-30 mb-8 mt-1 max-w-4xl text-center text-gray-450 lg:text-lg lg:leading-8">
Unify files from all your devices and clouds into a single, easy-to-use
explorer.
<br />
<span className="hidden sm:block">
Designed for creators, hoarders and the painfully disorganized.
</span>
</p>
<Downloads />
<div className="pb-6 xs:pb-24">
<div
className="xl2:relative z-30 flex h-[255px] w-full px-6
sm:h-[428px] md:mt-[75px] md:h-[428px] lg:h-auto"
>
<Image
loading="eager"
className="absolute-horizontal-center animation-delay-2 top-[380px] fade-in xs:top-[180px] md:top-[130px]"
width={1200}
height={626}
alt="l"
src="/images/appgradient.webp"
/>
<div className="relative m-auto mt-10 flex w-full max-w-7xl overflow-hidden rounded-lg transition-transform duration-700 ease-in-out hover:-translate-y-4 hover:scale-[1.02] md:mt-0">
<div className="z-30 flex w-full rounded-lg border-t border-app-line/50 backdrop-blur">
<CyclingImage
loading="eager"
width={1278}
height={626}
alt="spacedrive app"
className="rounded-lg"
images={[
'/images/app/1.webp',
'/images/app/2.webp',
'/images/app/3.webp',
'/images/app/4.webp',
'/images/app/5.webp',
'/images/app/10.webp',
'/images/app/6.webp',
'/images/app/7.webp',
'/images/app/8.webp',
'/images/app/9.webp'
]}
/>
<Image
loading="eager"
className="pointer-events-none absolute opacity-100 transition-opacity duration-1000 ease-in-out hover:opacity-0 md:w-auto"
width={2278}
height={626}
alt="l"
src="/images/appgradientoverlay.png"
/>
</div>
</div>
</div>
</div>
{/* <WormHole /> */}
{/* <BentoBoxes /> */}
{/* <CloudStorage /> */}
{/* <DownloadToday isWindows={deviceOs?.isWindows} /> */}
{/* <div className="h-[100px] sm:h-[200px] w-full" /> */}
</div>
</>
);
}

View file

@ -0,0 +1,13 @@
'use client';
import dynamic from 'next/dynamic';
const Space = dynamic(() => import('~/components/Space'), { ssr: false });
export function Background() {
return (
<div className="opacity-60">
<Space />
</div>
);
}

View file

@ -1,97 +1,67 @@
'use client';
import { Check } from '@phosphor-icons/react';
import clsx from 'clsx';
import Head from 'next/head';
import Image from 'next/image';
import React, { useState } from 'react';
import { useState } from 'react';
import { Button, Switch } from '@sd/ui';
import PageWrapper from '~/components/PageWrapper';
import { Space } from '~/components/Space';
export default function PricingPage() {
export function Cards() {
const [toggle, setToggle] = useState<boolean>(false);
return (
<>
<Head>
<title>Pricing - Spacedrive</title>
<meta name="description" content="Spacedrive pricing and packages" />
</Head>
<div className="opacity-60">
<Space />
<div className="fade-in-heading animation-delay-2 mx-auto flex w-full items-center justify-center gap-3">
<p className="text-sm font-medium text-white">Monthly</p>
<Switch checked={toggle} onCheckedChange={setToggle} size="lg" />
<p className="text-sm font-medium text-white">Yearly</p>
</div>
<PageWrapper>
<Image
loading="eager"
className="absolute-horizontal-center top-0 fade-in"
width={1278}
height={626}
alt="l"
src="/images/headergradient.webp"
<div
className="fade-in-heading animation-delay-2 mx-auto mb-[200px] mt-[75px] flex
w-full max-w-[1000px] flex-col items-center justify-center gap-10 px-2 md:flex-row"
>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
subTitle="Free for everyone"
toggle={toggle}
name="Free"
/>
<div className="z-5 relative mt-48">
<h1
className="fade-in-heading mb-3 bg-gradient-to-r from-white from-40% to-indigo-400 to-60% bg-clip-text px-2 text-center text-4xl
font-bold leading-tight text-transparent"
>
Pricing
</h1>
<p className="animation-delay-1 fade-in-heading text-md leading-2 z-30 mx-auto mb-8 mt-1 max-w-2xl px-2 text-center text-gray-450 lg:text-lg lg:leading-8">
Spacedrive can be used for free as you like. Upgrading gives you access to
early features, and this is placeholder text
</p>
<div className="fade-in-heading animation-delay-2 mx-auto flex w-full items-center justify-center gap-3">
<p className="text-sm font-medium text-white">Monthly</p>
<Switch onCheckedChange={setToggle} checked={toggle} size="lg" />
<p className="text-sm font-medium text-white">Yearly</p>
</div>
<div
className="fade-in-heading animation-delay-2 mx-auto mb-[200px] mt-[75px] flex
w-full max-w-[1000px] flex-col items-center justify-center gap-10 px-2 md:flex-row"
>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
subTitle="Free for everyone"
toggle={toggle}
name="Free"
/>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
toggle={toggle}
name="Pro"
price={{
monthly: '14.99',
yearly: '99.99'
}}
/>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
subTitle="Contact sales"
toggle={toggle}
name="Enterprise"
/>
</div>
</div>
</PageWrapper>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
toggle={toggle}
name="Pro"
price={{
monthly: '14.99',
yearly: '99.99'
}}
/>
<PackageCard
features={[
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text',
'lorem ipsum text'
]}
subTitle="Contact sales"
toggle={toggle}
name="Enterprise"
/>
</div>
</>
);
}

View file

@ -0,0 +1,39 @@
import Image from 'next/image';
import React from 'react';
import { Background } from './Background';
import { Cards } from './Cards';
export const metadata = {
title: 'Pricing - Spacedrive',
description: 'Spacedrive pricing and packages'
};
export default function PricingPage() {
return (
<>
<Background />
<Image
loading="eager"
className="absolute-horizontal-center top-0 fade-in"
width={1278}
height={626}
alt="l"
src="/images/headergradient.webp"
/>
<div className="z-5 relative mt-48">
<h1
className="fade-in-heading mb-3 bg-gradient-to-r from-white from-40% to-indigo-400 to-60% bg-clip-text px-2 text-center text-4xl
font-bold leading-tight text-transparent"
>
Pricing
</h1>
<p className="animation-delay-1 fade-in-heading text-md leading-2 z-30 mx-auto mb-8 mt-1 max-w-2xl px-2 text-center text-gray-450 lg:text-lg lg:leading-8">
Spacedrive can be used for free as you like. Upgrading gives you access to early
features, and this is placeholder text
</p>
<Cards />
</div>
</>
);
}

View file

@ -0,0 +1,114 @@
export const items = [
{
when: 'Big Bang',
subtext: 'Q1 2022',
completed: true,
title: 'File discovery',
description:
'Scan devices, drives and cloud accounts to build a directory of all files with metadata.'
},
{
title: 'Preview generation',
completed: true,
description: 'Auto generate lower resolution stand-ins for image and video.'
},
{
title: 'Statistics',
completed: true,
description: 'Total capacity, index size, preview media size, free space etc.'
},
{
title: 'Jobs',
completed: true,
description:
'Tasks to be performed via a queue system with multi-threaded workers, such as indexing, identifying, generating preview media and moving files. With a Job Manager interface for tracking progress, pausing and restarting jobs.'
},
{
completed: true,
title: 'Explorer',
description:
'Browse online/offline storage locations, view files with metadata, perform basic CRUD.'
},
{
completed: true,
title: 'Self hosting',
description:
'Spacedrive can be deployed as a service, behaving as just another device powering your personal cloud.'
},
{
completed: true,
title: 'Tags',
description:
'Define routines on custom tags to automate workflows, easily tag files individually, in bulk and automatically via rules.'
},
{
completed: true,
title: 'Search',
description: 'Deep search into your filesystem with a keybind, including offline locations.'
},
{
completed: true,
title: 'Media View',
description: 'Turn any directory into a camera roll including media from subdirectories'
},
{
when: '0.1.0 Alpha',
subtext: 'Oct 2023',
title: 'Key manager',
description:
'View, mount, unmount and hide keys. Mounted keys can be used to instantly encrypt and decrypt any files on your node.'
},
{
when: '0.2.0',
title: 'Spacedrop',
description: 'Drop files between devices and contacts on a keybind like AirDrop.'
},
{
title: 'Realtime library synchronization',
description: 'Automatically synchronized libraries across devices via P2P connections.'
},
{
when: '0.3.0',
title: 'Cloud integration',
description:
'Index & backup to Apple Photos, Google Drive, Dropbox, OneDrive & Mega + easy API for the community to add more.'
},
{
title: 'Media encoder',
description:
'Encode video and audio into various formats, use Tags to automate. Built with FFmpeg.'
},
{
title: 'Hosted Spaces',
description: 'Host select Spaces on our cloud to share with friends or publish on the web.'
},
{
when: '0.6.0 Beta',
subtext: 'Q3 2023',
title: 'Extensions',
description:
'Build tools on top of Spacedrive, extend functionality and integrate third party services. Extension directory on spacedrive.com/extensions.'
},
{
title: 'Encrypted vault(s)',
description:
'Effortlessly manage & encrypt sensitive files. Encrypt individual files or create flexible-size vaults.'
},
{
when: 'Release',
subtext: 'Q4 2023',
title: 'Timeline',
description:
'View a linear timeline of content, travel to any time and see media represented visually.'
},
{
title: 'Redundancy',
description:
'Ensure a specific amount of copies exist for your important data, discover at-risk files and monitor device/drive health.'
},
{
title: 'Workers',
description:
'Utilize the compute power of your devices in unison to encode and perform tasks at increased speeds.'
}
];

View file

@ -0,0 +1,91 @@
import clsx from 'clsx';
import Link from 'next/link';
import { Fragment } from 'react';
import { items } from './items';
export const metadata = {
title: 'Roadmap - Spacedrive',
description: 'What can Spacedrive do?'
};
export default function Page() {
return (
<div className="lg:prose-xs prose dark:prose-invert container m-auto mb-20 flex max-w-4xl flex-col gap-20 p-4 pt-32">
<section className="flex flex-col items-center">
<h1 className="fade-in-heading mb-0 text-center text-5xl leading-snug">
What's next for Spacedrive?
</h1>
<p className="animation-delay-2 fade-in-heading text-center text-gray-400">
Here is a list of the features we are working on, and the progress we have made
so far.
</p>
</section>
<section className="grid auto-cols-auto grid-flow-row grid-cols-[auto_1fr] gap-x-4">
{items.map((item, i) => (
<Fragment key={i}>
{/* Using span so i can use the group-last-of-type selector */}
<span className="group flex max-w-[10rem] items-start justify-end gap-4 first:items-start">
<div className="flex flex-col items-end">
<h3
className={
`m-0 hidden text-right lg:block ` +
(i === 0 ? '-translate-y-1/4' : '-translate-y-1/2')
}
>
{item.when}
</h3>
{item?.subtext && (
<span className="text-sm text-gray-300">{item?.subtext}</span>
)}
</div>
<div className="flex h-full w-2 group-first:mt-2 group-first:rounded-t-full group-last-of-type:rounded-b-full lg:items-center">
<div
className={
'flex h-full w-full ' +
(item.completed ? 'z-10 bg-primary-500' : 'bg-gray-550')
}
>
{item?.when !== undefined ? (
<div
className={clsx(
'absolute z-20 mt-5 h-4 w-4 -translate-x-1/4 -translate-y-1/2 rounded-full border-2 border-gray-200 group-first:mt-0 group-first:self-start lg:mt-0',
items[i - 1]?.completed || i === 0
? 'z-10 bg-primary-500'
: 'bg-gray-550'
)}
>
&zwj;
</div>
) : (
<div className="z-20">&zwj;</div>
)}
</div>
</div>
</span>
<div className="group flex flex-col items-start justify-center gap-4">
{item?.when && (
<h3 className="mb-0 group-first-of-type:m-0 lg:hidden">
{item.when}
</h3>
)}
<div className="my-2 flex w-full flex-col space-y-2 rounded-xl border border-gray-500 p-4 group-last:mb-0 group-first-of-type:mt-0">
<h3 className="m-0">{item.title}</h3>
<p>{item.description}</p>
</div>
</div>
</Fragment>
))}
</section>
<section className="space-y-2 rounded-xl bg-gray-850 p-8">
<h2 className="my-1">That's not all.</h2>
<p>
We're always open to ideas and feedback over{' '}
<Link href="https://github.com/spacedriveapp/spacedrive/discussions">here</Link>{' '}
and we have a <Link href="/blog">blog</Link> where you can find the latest news
and updates.
</p>
</section>
</div>
);
}

View file

@ -0,0 +1,83 @@
import { ArrowRight } from '@phosphor-icons/react/dist/ssr';
import Link from 'next/link';
import Markdown from '~/components/Markdown';
import { investors, teamMembers } from './people';
import { TeamMember } from './TeamMember';
export const metadata = {
title: 'Our Team - Spacedrive',
description: "Who's behind Spacedrive?"
};
export default function Page() {
return (
<Markdown articleClassNames="mx-auto mt-32 prose-a:text-white">
<div className="team-page relative mx-auto">
<div
className="bloom subtle egg-bloom-one -top-60 right-[-400px]"
style={{ transform: 'scale(2)' }}
/>
<div className="relative z-10">
<h1 className="fade-in-heading text-5xl leading-tight sm:leading-snug ">
We believe file management should be{' '}
<span className="title-gradient">universal</span>.
</h1>
<p className="animation-delay-2 fade-in-heading text-white/50 ">
Your priceless personal data shouldn't be stuck in a device ecosystem. It
should be OS agnostic, permanent and owned by you.
</p>
<p className="animation-delay-2 fade-in-heading text-white/50 ">
The data we create daily is our legacythat will long outlive us. Open
source technology is the only way to ensure we retain absolute control over
the files that define our lives, at unlimited scale.
</p>
<Link
href="/docs/product/resources/faq"
className="animation-delay-3 fade-in-heading text-underline flex flex-row items-center text-gray-400 underline-offset-4 duration-150 hover:text-white"
>
<ArrowRight className="mr-2" width="1rem" height="1rem" />
Read more
</Link>
<div className="fade-in-heading animation-delay-5">
<h2 className="mt-10 text-2xl leading-relaxed sm:mt-20 ">Meet the team</h2>
<div className="my-10 grid grid-cols-2 gap-x-5 gap-y-10 xs:grid-cols-3 sm:grid-cols-4">
{teamMembers.map((member) => (
<TeamMember key={member.name} {...member} />
))}
</div>
<p className="text-sm text-gray-400">
... and all the awesome{' '}
<Link
href="https://github.com/spacedriveapp/spacedrive/graphs/contributors"
target="_blank"
rel="noreferrer"
className="oss-credit-gradient duration-200 hover:opacity-75"
>
open source contributors
</Link>{' '}
on GitHub.
</p>
<h2
id="investors"
className="mb-2 mt-10 text-2xl leading-relaxed sm:mt-20 "
>
Our investors
</h2>
<p className="text-sm text-gray-400 ">
We're backed by some of the greatest leaders in the technology industry.
</p>
<div className="my-10 grid grid-cols-3 gap-x-5 gap-y-10 sm:grid-cols-5">
{investors.map((investor) => (
<TeamMember
key={investor.name + investor.investmentRound}
{...investor}
/>
))}
</div>
</div>
</div>
</div>
</Markdown>
);
}

View file

@ -1,9 +1,4 @@
import { ArrowRight } from '@phosphor-icons/react';
import Head from 'next/head';
import Link from 'next/link';
import Markdown from '~/components/Markdown';
import PageWrapper from '~/components/PageWrapper';
import { TeamMember, TeamMemberProps } from '~/components/TeamMember';
import { TeamMemberProps } from './TeamMember';
export const teamMembers: Array<TeamMemberProps> = [
{
@ -97,7 +92,7 @@ export const teamMembers: Array<TeamMemberProps> = [
}
];
const investors: Array<TeamMemberProps> = [
export const investors: Array<TeamMemberProps> = [
{
name: 'Joseph Jacks',
role: 'Founder, OSSC',
@ -207,84 +202,3 @@ const investors: Array<TeamMemberProps> = [
imageUrl: '/images/investors/naveen.jpg'
}
];
export default function TeamPage() {
return (
<PageWrapper>
<Markdown articleClassNames="mx-auto mt-32 prose-a:text-white">
<Head>
<title>Our Team - Spacedrive</title>
<meta name="description" content="Who's behind Spacedrive?" />
</Head>
<div className="team-page relative mx-auto">
<div
className="bloom subtle egg-bloom-one -top-60 right-[-400px]"
style={{ transform: 'scale(2)' }}
/>
<div className="relative z-10">
<h1 className="fade-in-heading text-5xl leading-tight sm:leading-snug ">
We believe file management should be{' '}
<span className="title-gradient">universal</span>.
</h1>
<p className="animation-delay-2 fade-in-heading text-white/50 ">
Your priceless personal data shouldn't be stuck in a device ecosystem.
It should be OS agnostic, permanent and owned by you.
</p>
<p className="animation-delay-2 fade-in-heading text-white/50 ">
The data we create daily is our legacythat will long outlive us. Open
source technology is the only way to ensure we retain absolute control
over the files that define our lives, at unlimited scale.
</p>
<Link
href="/docs/product/resources/faq"
className="animation-delay-3 fade-in-heading text-underline flex flex-row items-center text-gray-400 underline-offset-4 duration-150 hover:text-white"
>
<ArrowRight className="mr-2" />
Read more
</Link>
<div className="fade-in-heading animation-delay-5">
<h2 className="mt-10 text-2xl leading-relaxed sm:mt-20 ">
Meet the team
</h2>
<div className="my-10 grid grid-cols-2 gap-x-5 gap-y-10 xs:grid-cols-3 sm:grid-cols-4">
{teamMembers.map((member) => (
<TeamMember key={member.name} {...member} />
))}
</div>
<p className="text-sm text-gray-400">
... and all the awesome{' '}
<Link
href="https://github.com/spacedriveapp/spacedrive/graphs/contributors"
target="_blank"
rel="noreferrer"
className="oss-credit-gradient duration-200 hover:opacity-75"
>
open source contributors
</Link>{' '}
on GitHub.
</p>
<h2
id="investors"
className="mb-2 mt-10 text-2xl leading-relaxed sm:mt-20 "
>
Our investors
</h2>
<p className="text-sm text-gray-400 ">
We're backed by some of the greatest leaders in the technology
industry.
</p>
<div className="my-10 grid grid-cols-3 gap-x-5 gap-y-10 sm:grid-cols-5">
{investors.map((investor) => (
<TeamMember
key={investor.name + investor.investmentRound}
{...investor}
/>
))}
</div>
</div>
</div>
</div>
</Markdown>
</PageWrapper>
);
}

View file

@ -61,7 +61,7 @@ export const Bubbles = () => {
return (
<Particles
id="tsparticles"
className="absolute z-0"
className="absolute inset-0 z-0"
init={particlesInit}
options={options}
/>

View file

@ -1,3 +1,5 @@
'use client';
import Image, { ImageProps } from 'next/image';
import React, { useEffect, useState } from 'react';

View file

@ -12,11 +12,15 @@ import { PropsWithChildren } from 'react';
import Logo from '../../public/logo.png';
function FooterLink(props: PropsWithChildren<{ link: string; blank?: boolean }>) {
function FooterLink({
blank,
link,
...props
}: PropsWithChildren<{ link: string; blank?: boolean }>) {
return (
<Link
href={props.link}
target={props.blank ? '_blank' : ''}
href={link}
target={blank ? '_blank' : ''}
className="text-gray-300 duration-300 hover:text-white hover:opacity-50"
rel="noreferrer"
{...props}

View file

@ -1,8 +1,8 @@
import { IconProps } from '@phosphor-icons/react';
import clsx from 'clsx';
import { Button, ButtonBaseProps } from '@sd/ui';
import { Button, LinkButtonProps } from '@sd/ui';
interface Props extends ButtonBaseProps {
interface Props extends LinkButtonProps {
className?: string;
text: string;
icon?: IconProps;

View file

@ -6,7 +6,7 @@ import Link from 'next/link';
import { NextRouter, useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Button, Dropdown } from '@sd/ui';
import { positions } from '~/pages/careers';
import { positions } from '~/app/careers/data';
import { getWindow } from '~/utils/util';
import Logo from '../../public/logo.png';
@ -58,7 +58,7 @@ export default function NavBar() {
}, []);
return (
<div className={'navbar-blur fixed z-[55] h-16 w-full !bg-black/10 px-2 transition'}>
<div className="navbar-blur fixed z-[55] h-16 w-full !bg-black/10 px-2 transition">
<div className="relative m-auto flex h-full max-w-[100rem] items-center p-5">
<Link href="/" className="absolute flex flex-row items-center">
<Image alt="Spacedrive logo" src={Logo} className="z-30 mr-3 h-8 w-8" />

View file

@ -1,18 +1,23 @@
'use client';
import { PointMaterial, Points, Trail } from '@react-three/drei';
import { Canvas, useFrame } from '@react-three/fiber';
import { inSphere as randomInSphere } from 'maath/random';
import { useRef, useState, type FunctionComponent } from 'react';
import { useRef, useState } from 'react';
import { Color, type Mesh } from 'three';
import { hasWebGLContext } from '~/utils/util';
const Stars = (props: any) => {
const ref = useRef<Mesh>();
const [sphere] = useState(() => randomInSphere(new Float32Array(35000), { radius: 1 }));
useFrame((_, delta) => {
if (ref.current) {
ref.current.rotation.x -= delta / 300;
ref.current.rotation.y -= delta / 300;
}
});
return (
<group rotation={[0, 0, Math.PI / 4]}>
<Points ref={ref} positions={sphere} stride={3} frustumCulled={false} {...props}>
@ -30,6 +35,7 @@ const Stars = (props: any) => {
function ShootingStar() {
const ref = useRef<any>();
useFrame((state) => {
const t = state.clock.getElapsedTime() * 0.5;
if (ref.current) {
@ -40,6 +46,7 @@ function ShootingStar() {
);
}
});
return (
<Trail width={0.05} length={8} color={new Color(2, 1, 10)} attenuation={(t) => t * t}>
<mesh ref={ref}>
@ -50,7 +57,7 @@ function ShootingStar() {
);
}
export const Space: FunctionComponent = () => {
export default function Space() {
return (
<div className="absolute z-0 h-screen w-screen bg-black opacity-50">
<Canvas camera={{ position: [0, 0, 0] }}>
@ -60,4 +67,4 @@ export const Space: FunctionComponent = () => {
</Canvas>
</div>
);
};
}

View file

@ -1,4 +1,4 @@
import { Fire, Info } from '@phosphor-icons/react';
import { Fire, Info } from '@phosphor-icons/react/dist/ssr';
import clsx from 'clsx';
import MarkdownToJsx from 'markdown-to-jsx';

View file

@ -1,44 +0,0 @@
import { SmileyXEyes } from '@phosphor-icons/react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { Button } from '@sd/ui';
import Markdown from '~/components/Markdown';
import PageWrapper from '~/components/PageWrapper';
export default function Custom404Page() {
const router = useRouter();
return (
<PageWrapper>
<Markdown classNames="flex w-full justify-center">
<Head>
<title>Not Found - Spacedrive</title>
</Head>
<div className="m-auto flex flex-col items-center ">
<div className="h-32" />
<SmileyXEyes className="mb-3 h-44 w-44" />
<h1 className="mb-2 text-center">
In the quantum realm this page potentially exists.
</h1>
<p>In other words, thats a 404.</p>
<div className="flex flex-wrap justify-center">
<Button
onClick={() => router.back()}
className="mr-3 mt-2 cursor-pointer "
variant="gray"
>
Back
</Button>
<Button
href="/"
className="mt-2 cursor-pointer !text-white"
variant="accent"
>
Discover Spacedrive
</Button>
</div>
</div>
<div className="h-80" />
</Markdown>
</PageWrapper>
);
}

View file

@ -1,23 +0,0 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import Script from 'next/script';
import '@sd/ui/style';
import '~/styles/prism.css';
import '~/styles/style.scss';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<Component {...pageProps} />
<Script
src="/stats/js/script.js"
data-api="/stats/api/event"
data-domain="spacedrive.com"
/>
</>
);
}

View file

@ -1,37 +0,0 @@
import { Head, Html, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en" className="dark">
<Head>
<meta charSet="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="theme-color" content="#E751ED" media="not screen" />
<meta name="robots" content="index, follow" />
{/* For rendering math on docs */}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"
integrity="sha384-Xi8rHCmBmhbuyyhbI88391ZKP2dmfnOl4rT9ZfRI7mLTdk1wblIUnrIq35nqwEvC"
crossOrigin="anonymous"
/>
<meta
name="description"
content="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."
/>
<meta property="og:image" content="https://spacedrive.com/logo.png" />
<meta
name="keywords"
content="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"
/>
<meta name="author" content="Spacedrive Technology Inc." />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View file

@ -1,86 +0,0 @@
import { allPosts } from '@contentlayer/generated';
import dayjs from 'dayjs';
import { InferGetStaticPropsType } from 'next';
import { useMDXComponent } from 'next-contentlayer/hooks';
import Head from 'next/head';
import Image from 'next/image';
import { BlogTag } from '~/components/BlogTag';
import { BlogMDXComponents } from '~/components/mdx';
import PageWrapper from '~/components/PageWrapper';
export async function getStaticPaths() {
const paths = allPosts.map((post) => post.url);
return {
paths,
fallback: false
};
}
export async function getStaticProps({ params }: { params: { slug: string } }) {
const post = allPosts.find((post) => post.slug === params.slug);
if (!post) {
return {
notFound: true
};
}
return {
props: {
post
}
};
}
export default function PostPage({ post }: InferGetStaticPropsType<typeof getStaticProps>) {
const MDXContent = useMDXComponent(post.body.code);
const description =
post.excerpt?.length || 0 > 160 ? post.excerpt?.substring(0, 160) + '...' : post.excerpt;
return (
<PageWrapper>
<Head>
<title>{`${post.title} - Spacedrive Blog`}</title>
<meta name="description" content={description} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={post.image} />
<meta content="summary_large_image" name="twitter:card" />
<meta name="author" content={post.author} />
</Head>
<div className="lg:prose-xs prose dark:prose-invert container m-auto mb-20 max-w-4xl p-4 pt-14">
<>
<figure>
<Image
src={post.image}
alt={post.imageAlt ?? ''}
className="mt-8 rounded-xl"
height={400}
width={900}
/>
</figure>
<section className="-mx-8 flex flex-wrap gap-4 rounded-xl px-8">
<div className="w-full grow">
<h1 className="m-0 text-2xl leading-snug sm:text-4xl sm:leading-normal">
{post.title}
</h1>
<p className="m-0 mt-2">
by <b>{post.author}</b> &middot;{' '}
{dayjs(post.date).format('MM/DD/YYYY')}
</p>
</div>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<BlogTag key={tag} name={tag} />
))}
</div>
</section>
<article id="content" className="text-lg">
<MDXContent components={BlogMDXComponents} />
</article>
</>
</div>
</PageWrapper>
);
}

View file

@ -1,65 +0,0 @@
import { allPosts } from '@contentlayer/generated';
import dayjs from 'dayjs';
import { InferGetStaticPropsType } from 'next';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { BlogTag } from '~/components/BlogTag';
import PageWrapper from '~/components/PageWrapper';
export function getStaticProps() {
return { props: { posts: allPosts } };
}
export default function BlogPage({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<PageWrapper>
<div className="lg:prose-xs prose dark:prose-invert prose-a:no-underline container m-auto mb-20 flex max-w-4xl flex-col p-4 pt-32">
<Head>
<title>Spacedrive Blog</title>
<meta name="description" content="Get the latest from Spacedrive." />
</Head>
<section>
<h1 className="fade-in-heading m-0">Blog</h1>
<p className="fade-in-heading animation-delay-1">
Get the latest from Spacedrive.
</p>
</section>
<section className="animation-delay-2 mt-8 grid grid-cols-1 will-change-transform fade-in sm:grid-cols-1 lg:grid-cols-1">
{posts.map((post) => (
<Link
key={post.slug}
href={post.url}
className="relative z-0 mb-10 flex cursor-pointer flex-col gap-2 overflow-hidden rounded-xl border border-gray-500 transition-colors"
>
{post.image && (
<Image
src={post.image}
alt={post.imageAlt ?? ''}
className="inset-0 -z-10 m-0 w-full rounded-t-xl object-cover md:h-96"
// NOTE: Ideally we need to follow this specific ratio for our blog images
height={400}
width={800}
/>
)}
<div className="p-8">
<h2 className="text2xl m-0 md:text-4xl">{post.title}</h2>
<small className="m-0">{post.readTime}</small>
{/* <p className="line-clamp-3 my-2">{post.excerpt}</p> */}
<p className="m-0 text-white">
by {post.author} &middot;{' '}
{dayjs(post.date).format('MM/DD/YYYY')}
</p>
<div className="mt-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<BlogTag key={tag} name={tag} />
))}
</div>
</div>
</Link>
))}
</section>
</div>
</PageWrapper>
);
}

View file

@ -1,226 +0,0 @@
import {
Clock,
CurrencyDollar,
Desktop,
Heart,
House,
LightningSlash,
Smiley,
Star,
TrendUp
} from '@phosphor-icons/react';
import Head from 'next/head';
import { useRef } from 'react';
import { Button } from '@sd/ui';
import PageWrapper from '~/components/PageWrapper';
interface PositionPosting {
name: string;
type: string;
salary: string;
description: string;
}
export const positions: PositionPosting[] = [];
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
}
];
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'
}
];
export default function CareersPage() {
const openPositionsRef = useRef<HTMLHRElement>(null);
const scrollToPositions = () =>
openPositionsRef.current?.scrollIntoView({ behavior: 'smooth' });
return (
<PageWrapper>
<Head>
<title>Careers - Spacedrive</title>
<meta
name="description"
content="Work with us to build the future of file management."
/>
</Head>
<div className="prose prose-invert container relative m-auto mb-20 min-h-screen max-w-4xl p-4 pt-32 text-white">
<div
className="bloom subtle egg-bloom-two -top-60 right-[-400px]"
style={{ transform: 'scale(2)' }}
/>
<h1 className="fade-in-heading mb-3 px-2 text-center text-4xl font-black leading-tight text-white md:text-5xl">
Build the future of files.
</h1>
<div className="animation-delay-1 z-30 flex flex-col items-center fade-in">
<p className="z-40 text-center text-lg text-gray-350">
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.
</p>
<Button
onClick={scrollToPositions}
className="z-30 cursor-pointer border-0"
variant="accent"
>
See Open Positions
</Button>
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-4xl font-black leading-tight">
Our Values
</h2>
<p className="mb-4 mt-2">What drives us daily.</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
{values.map((value, index) => (
<div
key={value.title + index}
className="flex flex-col rounded-md border border-gray-500 bg-gray-550/50 p-10"
>
<value.icon className="text-[32px]" weight="bold" />
<h3 className="mb-1 mt-4 text-2xl font-bold leading-snug">
{value.title}
</h3>
<p className="mb-0 mt-1 text-gray-350">{value.desc}</p>
</div>
))}
</div>
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-4xl font-black leading-tight text-white">
Perks and Benefits
</h2>
<p className="mb-4 mt-2">We're behind you 100%.</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4 sm:grid-cols-3">
{perks.map((value, index) => (
<div
key={value.title + index}
style={{
backgroundColor: value.color + '10',
borderColor: value.color + '30'
}}
className="flex flex-col rounded-md border bg-gray-550/30 p-8"
>
<value.icon
className="text-[32px]"
weight="bold"
color={value.color}
/>
<h3 className="mb-1 mt-4">{value.title}</h3>
<p className="mb-0 mt-1 text-sm text-white opacity-60">
{value.desc}
</p>
</div>
))}
</div>
<hr
className="border-1 my-24 w-full border-gray-200 opacity-10"
ref={openPositionsRef}
/>
<h2 className="mb-0 px-2 text-center text-4xl font-black leading-tight text-white">
Open Positions
</h2>
{positions.length === 0 ? (
<p className="mt-2 text-center text-gray-350">
There are no positions open at this time. Please check back later!
</p>
) : (
<>
<p className="mb-4 mt-2">If any open positions suit you, apply now!</p>
<div className="mt-5 grid w-full grid-cols-1 gap-4">
{positions.map((value, index) => (
<div
key={value.name + index}
className="flex flex-col rounded-md border border-gray-500 bg-gray-550/50 p-10"
>
<div className="flex flex-col sm:flex-row">
<h3 className="m-0 text-2xl leading-tight">
{value.name}
</h3>
<div className="mt-3 sm:mt-0.5">
<span className="text-sm font-semibold text-gray-300 sm:ml-4">
<CurrencyDollar className="-mt-1 mr-1 inline w-4" />
{value.salary}
</span>
<span className="ml-4 text-sm font-semibold text-gray-300">
<Clock className="-mt-1 mr-1 inline w-4" />
{value.type}
</span>
</div>
</div>
<p className="mb-0 mt-3 text-gray-350">
{value.description}
</p>
</div>
))}
</div>
</>
)}
<hr className="border-1 my-24 w-full border-gray-200 opacity-10" />
<h2 className="mb-0 px-2 text-center text-3xl font-black text-white">
How to apply?
</h2>
<p className="mt-2">
Send your cover letter and resume to{' '}
<strong>careers at spacedrive dot com</strong> and we'll get back to you
shortly!
</p>
</div>
</div>
</PageWrapper>
);
}

View file

@ -1,113 +0,0 @@
import { allDocs } from '@contentlayer/generated';
import { CaretRight } from '@phosphor-icons/react';
import { Github } from '@sd/assets/svgs/brands';
import { InferGetStaticPropsType } from 'next';
import { useMDXComponent } from 'next-contentlayer/hooks';
import Head from 'next/head';
import Link from 'next/link';
import { PropsWithChildren } from 'react';
import DocsLayout from '~/components/DocsLayout';
import Markdown from '~/components/Markdown';
import { DocMDXComponents } from '~/components/mdx';
import PageWrapper from '~/components/PageWrapper';
import { getDocsNavigation } from '~/utils/contentlayer';
import { toTitleCase } from '~/utils/util';
export async function getStaticPaths() {
const paths = allDocs.map((doc) => doc.url);
return {
paths,
fallback: false
};
}
export async function getStaticProps({ params }: { params: { slug: string[] } }) {
const slug = params.slug.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 {
props: {
navigation: docNavigation,
doc,
nextDoc
}
};
}
function BottomCard(props: PropsWithChildren) {
return (
<div className="group flex flex-row items-center rounded-lg border border-gray-700 p-4 text-sm !text-gray-200 transition-all duration-200 hover:translate-y-[-2px] hover:border-primary hover:!text-primary hover:shadow-xl hover:shadow-primary/10">
{props.children}
</div>
);
}
export default function DocPage({
navigation,
doc,
nextDoc
}: InferGetStaticPropsType<typeof getStaticProps>) {
const MDXContent = useMDXComponent(doc.body.code);
return (
<PageWrapper>
<Head>
<title>{`${doc.title} - Spacedrive Documentation`}</title>
<meta name="description" content={doc.description} />
<meta property="og:title" content={doc.title} />
<meta property="og:description" content={doc.description} />
<meta name="author" content={'Spacedrive Technology Inc.'} />
</Head>
<DocsLayout docUrl={doc.url} navigation={navigation}>
<Markdown classNames="sm:mt-[105px] mt-6 min-h-screen ">
<h5 className="mb-2 text-sm font-semibold text-primary lg:min-w-[700px]">
{toTitleCase(doc.category)}
</h5>
<MDXContent components={DocMDXComponents} />
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
<Link
target="_blank"
rel="noreferrer"
href={`https://github.com/spacedriveapp/spacedrive/blob/main${doc.url}.mdx`}
className="w-full"
>
<BottomCard>
<Github className="mr-3 w-5" />
Edit this page on GitHub
</BottomCard>
</Link>
{nextDoc && (
<Link href={nextDoc.url} className="w-full">
<BottomCard>
<CaretRight className="mr-3 w-5" />
Next article: {nextDoc.title}
</BottomCard>
</Link>
)}
</div>
</Markdown>
</DocsLayout>
</PageWrapper>
);
}

View file

@ -1,43 +0,0 @@
import { allDocs } from '@contentlayer/generated';
import { InferGetStaticPropsType } from 'next';
import Head from 'next/head';
import Link from 'next/link';
import DocsLayout from '~/components/DocsLayout';
import Markdown from '~/components/Markdown';
import PageWrapper from '~/components/PageWrapper';
import { getDocsNavigation } from '~/utils/contentlayer';
export function getStaticProps() {
return { props: { navigation: getDocsNavigation(allDocs) } };
}
export default function DocHomePage({
navigation
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<PageWrapper>
<Head>
<title>Spacedrive Docs</title>
<meta name="description" content="Learn more about Spacedrive" />
</Head>
<DocsLayout navigation={navigation}>
<Markdown>
<div className="mt-[105px]">
<h1 className="text-4xl font-bold">Spacedrive Docs</h1>
<p className="text-lg text-gray-400">
Welcome to the Spacedrive documentation. Here you can find all the
information you need to get started with Spacedrive.
</p>
<Link
className="text-primary-600 transition hover:text-primary-500"
href="/docs/product/getting-started/introduction"
>
Get Started
</Link>
</div>
</Markdown>
</DocsLayout>
</PageWrapper>
);
}

View file

@ -1,400 +0,0 @@
import { AndroidLogo, Globe, LinuxLogo, WindowsLogo } from '@phosphor-icons/react';
import { Apple, Github } from '@sd/assets/svgs/brands';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import Image from 'next/image';
import { ComponentProps, useEffect, useState } from 'react';
import { Tooltip, TooltipProvider, tw } from '@sd/ui';
import NewBanner from '~/components/NewBanner';
import PageWrapper from '~/components/PageWrapper';
import { detectWebGLContext, getWindow } from '~/utils/util';
import CyclingImage from '../components/CyclingImage';
const HomeCTA = dynamic(() => import('~/components/HomeCTA'), {
ssr: false
});
const AppFrameOuter = tw.div`relative m-auto flex w-full max-w-7xl rounded-lg transition-opacity`;
const AppFrameInner = tw.div`z-30 flex w-full rounded-lg border-t border-app-line/50 backdrop-blur`;
const RELEASE_VERSION = 'Alpha v0.1.0';
const BASE_DL_LINK = '/api/releases/desktop/stable';
const downloadEntries = {
linux: {
name: 'Linux',
icon: <LinuxLogo />,
links: 'linux/x86_64'
},
macOS: {
name: 'macOS',
icon: <Apple />,
links: {
'Intel': 'darwin/x86_64',
'Apple Silicon': 'darwin/aarch64'
}
},
windows: {
name: 'Windows',
icon: <WindowsLogo />,
links: 'windows/x86_64'
}
} as const;
const platforms = [
{ name: 'macOS', icon: Apple, clickable: true, version: '12+' },
{
name: 'Windows',
icon: WindowsLogo,
href: `${BASE_DL_LINK}/${downloadEntries.windows.links}`,
version: '10+'
},
{
name: 'Linux',
icon: LinuxLogo,
href: `${BASE_DL_LINK}/${downloadEntries.linux.links}`,
version: 'AppImage'
},
{ name: 'Android', icon: AndroidLogo, version: '10+' },
{ name: 'Web', icon: Globe }
] as const;
export default function HomePage() {
const [opacity, setOpacity] = useState(0.6);
const [background, setBackground] = useState<JSX.Element | null>(null);
const [multipleDownloads, setMultipleDownloads] =
useState<(typeof downloadEntries)['macOS']['links']>();
const [downloadEntry, setDownloadEntry] =
useState<(typeof downloadEntries)['linux' | 'macOS' | 'windows']>();
const [isWindowResizing, setIsWindowResizing] = useState(false);
const links = downloadEntry?.links;
useEffect(() => {
import('react-device-detect').then(({ isWindows, isMacOs, isMobile }) => {
if (isWindows) {
setDownloadEntry(downloadEntries.windows);
} else if (isMacOs) {
setDownloadEntry(downloadEntries.macOS);
} else if (!isMobile) {
setDownloadEntry(downloadEntries.linux);
}
});
const fadeStart = 300; // start fading out at 100px
const fadeEnd = 1300; // end fading out at 300px
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY <= fadeStart) {
setOpacity(0.6);
} else if (currentScrollY <= fadeEnd) {
const range = fadeEnd - fadeStart;
const diff = currentScrollY - fadeStart;
const ratio = diff / range;
setOpacity(0.6 - ratio);
} else {
setOpacity(0);
}
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
let resizeTimer: NodeJS.Timeout;
const handleResize = () => {
setIsWindowResizing(true);
setBackground(null);
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
setIsWindowResizing(false);
}, 100);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
clearTimeout(resizeTimer);
};
}, []);
useEffect(() => {
if (isWindowResizing) return;
if (!(getWindow() && background == null)) return;
(async () => {
if (detectWebGLContext()) {
const Space = (await import('~/components/Space')).Space;
setBackground(<Space />);
} else {
console.warn('Fallback to Bubbles background due WebGL not being available');
const Bubbles = (await import('~/components/Bubbles')).Bubbles;
setBackground(<Bubbles />);
}
})();
}, [background, isWindowResizing]);
const currentPlatform =
downloadEntry !== undefined
? platforms.find((e) => e.name === downloadEntry.name)
: undefined;
const supportedVersion =
currentPlatform && 'version' in currentPlatform ? currentPlatform.version : undefined;
const formattedVersion =
downloadEntry && supportedVersion && downloadEntry.name !== 'Linux'
? `${downloadEntry.name} ${supportedVersion}`
: supportedVersion;
return (
<TooltipProvider>
<Head>
<title>Spacedrive A file manager from the future.</title>
<meta
name="description"
content="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."
/>
<meta
property="og:image"
content="https://raw.githubusercontent.com/spacedriveapp/.github/main/profile/spacedrive_icon.png"
/>
<meta
name="keywords"
content="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"
/>
<meta name="author" content="Spacedrive Technology Inc." />
</Head>
<div style={{ opacity }}>{background}</div>
<PageWrapper>
{/* <div
className="absolute-horizontal-center h-[140px] w-[60%] overflow-hidden
rounded-full bg-gradient-to-r from-indigo-500 to-fuchsia-500 opacity-60 blur-[80px] md:blur-[150px]"
/> */}
<Image
loading="eager"
className="absolute-horizontal-center fade-in"
width={1278}
height={626}
alt="l"
src="/images/headergradient.webp"
/>
<div className="flex w-full flex-col items-center px-4">
<div className="mt-22 lg:mt-28" id="content" aria-hidden="true" />
<div className="mt-24 lg:mt-8" />
<NewBanner
headline="Alpha release is finally here!"
href="/blog/october-alpha-release"
link="Read post"
className="mt-[50px] lg:mt-0"
/>
<h1 className="fade-in-heading z-30 mb-3 bg-clip-text px-2 text-center text-4xl font-bold leading-tight text-white md:text-5xl lg:text-7xl">
One Explorer. All Your Files.
</h1>
<p className="animation-delay-1 fade-in-heading text-md leading-2 z-30 mb-8 mt-1 max-w-4xl text-center text-gray-450 lg:text-lg lg:leading-8">
Unify files from all your devices and clouds into a single, easy-to-use
explorer.
<br />
<span className="hidden sm:block">
Designed for creators, hoarders and the painfully disorganized.
</span>
</p>
<div className="flex flex-row gap-3">
{!(downloadEntry && links) ? null : typeof links === 'string' ? (
<a
target="_blank"
href={`${BASE_DL_LINK}/${links}`}
className={`plausible-event-name=download plausible-event-os=${downloadEntry.name}`}
>
<HomeCTA
icon={downloadEntry.icon}
text={`Download for ${downloadEntry.name}`}
className="z-5 relative"
/>
</a>
) : (
<HomeCTA
icon={downloadEntry.icon}
text={`Download for ${downloadEntry.name}`}
onClick={() =>
setMultipleDownloads(multipleDownloads ? undefined : links)
}
/>
)}
<a target="_blank" href="https://www.github.com/spacedriveapp/spacedrive">
<HomeCTA
icon={<Github />}
className="z-5 relative"
text="Star on GitHub"
/>
</a>
</div>
{multipleDownloads && (
<div className="z-50 mb-2 mt-4 flex flex-row gap-3 fade-in">
{Object.entries(multipleDownloads).map(([name, link]) => (
<a
key={name}
target="_blank"
href={`${BASE_DL_LINK}/${link}`}
className={`plausible-event-name=download plausible-event-os=macOS+${
link.split('/')[1]
}`}
>
<HomeCTA
size="md"
text={name}
className="z-5 relative !py-1 !text-sm"
/>
</a>
))}
</div>
)}
<p
className={
'animation-delay-3 z-30 mt-3 px-6 text-center text-sm text-gray-400 fade-in'
}
>
{RELEASE_VERSION}
{formattedVersion && (
<>
<span className="mx-2 opacity-50">|</span>
{formattedVersion}
</>
)}
</p>
{/* Platforms */}
<div className="relative z-10 mt-5 flex gap-3">
{platforms.map((platform, i) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.2, ease: 'easeInOut' }}
key={platform.name}
>
<Platform
icon={platform.icon}
label={platform.name}
className={clsx(
platform.name !== 'macOS' &&
`plausible-event-name=download plausible-event-os=${platform.name}`
)}
href={'href' in platform ? platform.href : undefined}
iconDisabled={
platform.name === 'Android' || platform.name === 'Web'
? true
: undefined
}
clickable={
'clickable' in platform ? platform.clickable : undefined
}
onClick={() => {
if (platform.name === 'macOS') {
setMultipleDownloads(
multipleDownloads
? undefined
: downloadEntries.macOS.links
);
}
}}
/>
</motion.div>
))}
</div>
<div className="pb-6 xs:pb-24">
<div
className="xl2:relative z-30 flex h-[255px] w-full px-6
sm:h-[428px] md:mt-[75px] md:h-[428px] lg:h-auto"
>
<Image
loading="eager"
className="absolute-horizontal-center animation-delay-2 top-[380px] fade-in xs:top-[180px] md:top-[130px]"
width={1200}
height={626}
alt="l"
src="/images/appgradient.webp"
/>
<AppFrameOuter
className=" relative mt-10 overflow-hidden
transition-transform duration-700 ease-in-out hover:-translate-y-4 hover:scale-[1.02] md:mt-0"
>
<AppFrameInner>
<CyclingImage
loading="eager"
width={1278}
height={626}
alt="spacedrive app"
className="rounded-lg"
images={[
'/images/app/1.webp',
'/images/app/2.webp',
'/images/app/3.webp',
'/images/app/4.webp',
'/images/app/5.webp',
'/images/app/10.webp',
'/images/app/6.webp',
'/images/app/7.webp',
'/images/app/8.webp',
'/images/app/9.webp'
]}
/>
<Image
loading="eager"
className="pointer-events-none absolute opacity-100 transition-opacity duration-1000 ease-in-out hover:opacity-0 md:w-auto"
width={2278}
height={626}
alt="l"
src="/images/appgradientoverlay.png"
/>
</AppFrameInner>
</AppFrameOuter>
</div>
</div>
{/* <WormHole /> */}
{/* <BentoBoxes /> */}
{/* <CloudStorage /> */}
{/* <DownloadToday isWindows={deviceOs?.isWindows} /> */}
{/* <div className="h-[100px] sm:h-[200px] w-full" /> */}
</div>
</PageWrapper>
</TooltipProvider>
);
}
interface Props {
label: string;
icon: any;
iconDisabled?: true;
clickable?: true;
}
const Platform = ({ icon: Icon, label, ...props }: ComponentProps<'a'> & Props) => {
const Outer = props.href
? (props: any) => <a aria-label={label} target="_blank" {...props} />
: props.clickable
? (props: any) => <button {...props} />
: ({ children }: any) => <>{children}</>;
return (
<Tooltip label={label}>
<Outer {...props}>
<Icon
size={25}
className={`h-[25px] ${props.iconDisabled ? 'opacity-20' : 'opacity-80'}`}
weight="fill"
/>
</Outer>
</Tooltip>
);
};

View file

@ -1,213 +0,0 @@
import clsx from 'clsx';
import Head from 'next/head';
import Link from 'next/link';
import { Fragment } from 'react';
import PageWrapper from '~/components/PageWrapper';
const items = [
{
when: 'Big Bang',
subtext: 'Q1 2022',
completed: true,
title: 'File discovery',
description:
'Scan devices, drives and cloud accounts to build a directory of all files with metadata.'
},
{
title: 'Preview generation',
completed: true,
description: 'Auto generate lower resolution stand-ins for image and video.'
},
{
title: 'Statistics',
completed: true,
description: 'Total capacity, index size, preview media size, free space etc.'
},
{
title: 'Jobs',
completed: true,
description:
'Tasks to be performed via a queue system with multi-threaded workers, such as indexing, identifying, generating preview media and moving files. With a Job Manager interface for tracking progress, pausing and restarting jobs.'
},
{
completed: true,
title: 'Explorer',
description:
'Browse online/offline storage locations, view files with metadata, perform basic CRUD.'
},
{
completed: true,
title: 'Self hosting',
description:
'Spacedrive can be deployed as a service, behaving as just another device powering your personal cloud.'
},
{
completed: true,
title: 'Tags',
description:
'Define routines on custom tags to automate workflows, easily tag files individually, in bulk and automatically via rules.'
},
{
completed: true,
title: 'Search',
description: 'Deep search into your filesystem with a keybind, including offline locations.'
},
{
completed: true,
title: 'Media View',
description: 'Turn any directory into a camera roll including media from subdirectories'
},
{
when: '0.1.0 Alpha',
subtext: 'Oct 2023',
title: 'Key manager',
description:
'View, mount, unmount and hide keys. Mounted keys can be used to instantly encrypt and decrypt any files on your node.'
},
{
when: '0.2.0',
title: 'Spacedrop',
description: 'Drop files between devices and contacts on a keybind like AirDrop.'
},
{
title: 'Realtime library synchronization',
description: 'Automatically synchronized libraries across devices via P2P connections.'
},
{
when: '0.3.0',
title: 'Cloud integration',
description:
'Index & backup to Apple Photos, Google Drive, Dropbox, OneDrive & Mega + easy API for the community to add more.'
},
{
title: 'Media encoder',
description:
'Encode video and audio into various formats, use Tags to automate. Built with FFmpeg.'
},
{
title: 'Hosted Spaces',
description: 'Host select Spaces on our cloud to share with friends or publish on the web.'
},
{
when: '0.6.0 Beta',
subtext: 'Q3 2023',
title: 'Extensions',
description:
'Build tools on top of Spacedrive, extend functionality and integrate third party services. Extension directory on spacedrive.com/extensions.'
},
{
title: 'Encrypted vault(s)',
description:
'Effortlessly manage & encrypt sensitive files. Encrypt individual files or create flexible-size vaults.'
},
{
when: 'Release',
subtext: 'Q4 2023',
title: 'Timeline',
description:
'View a linear timeline of content, travel to any time and see media represented visually.'
},
{
title: 'Redundancy',
description:
'Ensure a specific amount of copies exist for your important data, discover at-risk files and monitor device/drive health.'
},
{
title: 'Workers',
description:
'Utilize the compute power of your devices in unison to encode and perform tasks at increased speeds.'
}
];
export default function RoadmapPage() {
return (
<PageWrapper>
<Head>
<title>Roadmap - Spacedrive</title>
<meta name="description" content="What can Spacedrive do?" />
</Head>
<div className="lg:prose-xs prose dark:prose-invert container m-auto mb-20 flex max-w-4xl flex-col gap-20 p-4 pt-32">
<section className="flex flex-col items-center">
{/* ??? why img tag */}
<img className="pointer-events-none w-24" />
<h1 className="fade-in-heading mb-0 text-center text-5xl leading-snug">
What's next for Spacedrive?
</h1>
<p className="animation-delay-2 fade-in-heading text-center text-gray-400">
Here is a list of the features we are working on, and the progress we have
made so far.
</p>
</section>
<section className="grid auto-cols-auto grid-flow-row grid-cols-[auto_1fr] gap-x-4">
{items.map((item, i) => (
<Fragment key={i}>
{/* Using span so i can use the group-last-of-type selector */}
<span className="group flex max-w-[10rem] items-start justify-end gap-4 first:items-start">
<div className="flex flex-col items-end">
<h3
className={
`m-0 hidden text-right lg:block ` +
(i === 0 ? '-translate-y-1/4' : '-translate-y-1/2')
}
>
{item.when}
</h3>
{item?.subtext && (
<span className="text-sm text-gray-300">
{item?.subtext}
</span>
)}
</div>
<div className="flex h-full w-2 group-first:mt-2 group-first:rounded-t-full group-last-of-type:rounded-b-full lg:items-center">
<div
className={
'flex h-full w-full ' +
(item.completed ? 'z-10 bg-primary-500' : 'bg-gray-550')
}
>
{item?.when !== undefined ? (
<div
className={clsx(
'absolute z-20 mt-5 h-4 w-4 -translate-x-1/4 -translate-y-1/2 rounded-full border-2 border-gray-200 group-first:mt-0 group-first:self-start lg:mt-0',
items[i - 1]?.completed || i === 0
? 'z-10 bg-primary-500'
: 'bg-gray-550'
)}
>
&zwj;
</div>
) : (
<div className="z-20">&zwj;</div>
)}
</div>
</div>
</span>
<div className="group flex flex-col items-start justify-center gap-4">
{item?.when && (
<h3 className="mb-0 group-first-of-type:m-0 lg:hidden">
{item.when}
</h3>
)}
<div className="my-2 flex w-full flex-col space-y-2 rounded-xl border border-gray-500 p-4 group-last:mb-0 group-first-of-type:mt-0">
<h3 className="m-0">{item.title}</h3>
<p>{item.description}</p>
</div>
</div>
</Fragment>
))}
</section>
<section className="space-y-2 rounded-xl bg-gray-850 p-8">
<h2 className="my-1">That's not all.</h2>
<p>
We're always open to ideas and feedback over{' '}
<Link href="https://github.com/spacedriveapp/spacedrive/discussions">
here
</Link>{' '}
and we have a <Link href="/blog">blog</Link> where you can find the latest
news and updates.
</p>
</section>
</div>
</PageWrapper>
);
}

View file

@ -1,33 +0,0 @@
import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses';
import { env } from '~/env';
export const ses = new SESClient({
region: env.AWS_SES_REGION,
credentials: {
accessKeyId: env.AWS_SES_ACCESS_KEY,
secretAccessKey: env.AWS_SES_SECRET_KEY
}
});
export async function sendEmail(email: string, subject: string, body: string) {
await ses.send(
new SendEmailCommand({
Destination: {
ToAddresses: [email]
},
Message: {
Body: {
Html: {
Charset: 'UTF-8',
Data: body
}
},
Subject: {
Charset: 'UTF-8',
Data: subject
}
},
Source: env.MAILER_FROM
})
);
}

View file

@ -1,25 +0,0 @@
import { connect } from '@planetscale/database';
import { mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core';
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { env } from '~/env';
export { and, eq, or, type InferModel } from 'drizzle-orm';
const dbConnection = connect({
url: env.DATABASE_URL
});
export const db = drizzle(dbConnection);
// Spacedrive Schema
export const waitlistTable = mysqlTable('waitlist', {
id: serial('id').primaryKey(),
cuid: varchar('cuid', {
length: 26
}).notNull(),
email: varchar('email', {
length: 255
}).notNull(),
created_at: timestamp('created_at').notNull()
});

View file

@ -1,5 +1,6 @@
import { Doc, DocumentTypes } from '@contentlayer/generated';
import { Circle, Cube, Icon, Sparkle, Star } from '@phosphor-icons/react';
import { type Icon } from '@phosphor-icons/react';
import { Circle, Cube, Sparkle, Star } from '@phosphor-icons/react/dist/ssr';
import { toTitleCase } from './util';
@ -10,10 +11,12 @@ type DocsCategory = {
docs: CoreContent<Doc>[];
};
export type DocsNavigation = {
type DocsSection = {
slug: string;
categories: DocsCategory[];
}[];
};
export type DocsNavigation = DocsSection[];
export function getDocsNavigation(docs: Doc[]): DocsNavigation {
const coreDocs = allCoreContent(docs);

View file

@ -17,11 +17,16 @@ export function toTitleCase(str: string) {
// https://github.com/mrdoob/three.js/blob/7fa8637df3edcf21a516e1ebbb9b327136457baa/src/renderers/WebGLRenderer.js#L266
const webGLCtxNames = ['webgl2', 'webgl', 'experimental-webgl'];
export function detectWebGLContext() {
export function hasWebGLContext(): boolean {
const window = getWindow();
if (!window) return false;
const canvas = window?.document.createElement('canvas');
if (!canvas) return false;
const { WebGLRenderingContext, WebGL2RenderingContext } = window;
if (WebGLRenderingContext == null) return false;
const canvas = window.document.createElement('canvas');
return webGLCtxNames
.map((ctxName) => {
try {
@ -34,7 +39,7 @@ export function detectWebGLContext() {
(ctx) =>
ctx != null &&
(ctx instanceof WebGLRenderingContext ||
(WebGL2RenderingContext != null && ctx instanceof WebGL2RenderingContext)) &&
ctx.getParameter(ctx.VERSION) != null
(WebGL2RenderingContext !== null && ctx instanceof WebGL2RenderingContext)) &&
ctx.getParameter(ctx.VERSION) !== null
);
}

View file

@ -1,6 +1,6 @@
import type { Preview } from '@storybook/react';
import '@sd/ui/style';
import '@sd/ui/style.scss';
const preview: Preview = {
parameters: {

View file

@ -2,7 +2,7 @@
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import '@sd/ui/style';
import '@sd/ui/style/style.scss';
import '~/patches';
import App from './App';

View file

@ -1,12 +1,29 @@
import clsx from 'clsx';
import { getPasswordStrength } from '@sd/client';
import { useEffect, useState } from 'react';
import { type getPasswordStrength } from '@sd/client';
export interface PasswordMeterProps {
password: string;
}
export const PasswordMeter = (props: PasswordMeterProps) => {
const { score, scoreText } = getPasswordStrength(props.password);
const [getStrength, setGetStrength] = useState<typeof getPasswordStrength | undefined>();
const { score, scoreText } = getStrength
? getStrength(props.password)
: { score: 0, scoreText: 'Loading...' };
useEffect(() => {
let cancelled = false;
import('@sd/client').then(({ getPasswordStrength }) => {
if (cancelled) return;
setGetStrength(() => getPasswordStrength);
});
return () => {
cancelled = true;
};
}, []);
return (
<div className="relative">

View file

@ -3,6 +3,7 @@
"private": true,
"main": "index.tsx",
"types": "index.tsx",
"sideEffects": false,
"scripts": {
"lint": "eslint . --cache",
"typecheck": "tsc -b"
@ -10,7 +11,7 @@
"dependencies": {
"@fontsource/inter": "^4.5.13",
"@headlessui/react": "^1.7.3",
"@icons-pack/react-simple-icons": "^7.2.0",
"@icons-pack/react-simple-icons": "^9.1.0",
"@phosphor-icons/react": "^2.0.10",
"@radix-ui/react-progress": "^1.0.1",
"@radix-ui/react-slider": "^1.1.0",

View file

@ -3,6 +3,7 @@
"version": "1.0.0",
"license": "GPL-3.0-only",
"private": true,
"sideEffects": false,
"scripts": {
"gen": "node ./scripts/generate.mjs"
}

View file

@ -3,6 +3,8 @@
* To regenerate this file, run: pnpm assets gen
*/
'use client';
import { ReactComponent as Academia } from './Academia.svg';
import { ReactComponent as Apple } from './apple.svg';
import { ReactComponent as Discord } from './Discord.svg';

View file

@ -1,6 +1,7 @@
{
"name": "@sd/client",
"private": true,
"sideEffects": false,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {

View file

@ -4,6 +4,7 @@
"license": "GPL-3.0-only",
"main": "src/index.ts",
"types": "src/index.ts",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./src/forms": "./src/forms/index.ts",

View file

@ -1,9 +1,11 @@
'use client';
import { cva, cx, VariantProps } from 'class-variance-authority';
import clsx from 'clsx';
import { ComponentProps, forwardRef } from 'react';
import { Link } from 'react-router-dom';
export interface ButtonBaseProps extends VariantProps<typeof styles> {}
export interface ButtonBaseProps extends VariantProps<typeof buttonStyles> {}
export type ButtonProps = ButtonBaseProps &
React.ButtonHTMLAttributes<HTMLButtonElement> & {
@ -22,7 +24,7 @@ type Button = {
const hasHref = (props: ButtonProps | LinkButtonProps): props is LinkButtonProps => 'href' in props;
export const styles = cva(
export const buttonStyles = cva(
[
'cursor-default items-center rounded-md border outline-none transition-colors duration-100',
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-70',
@ -73,7 +75,7 @@ export const Button = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
ButtonProps | LinkButtonProps
>(({ className, ...props }, ref) => {
className = cx(styles(props), className);
className = cx(buttonStyles(props), className);
return hasHref(props) ? (
<a {...props} ref={ref as any} className={cx(className, 'inline-block no-underline')} />
) : (
@ -88,7 +90,7 @@ export const ButtonLink = forwardRef<
return (
<Link
ref={ref}
className={styles({
className={buttonStyles({
size,
variant,
className: clsx(

View file

@ -1,3 +1,5 @@
'use client';
import { Check } from '@phosphor-icons/react';
import * as Checkbox from '@radix-ui/react-checkbox';
import { cva, VariantProps } from 'class-variance-authority';

View file

@ -1,3 +1,5 @@
'use client';
import { CaretRight, Check, Icon, IconProps } from '@phosphor-icons/react';
import * as RadixCM from '@radix-ui/react-context-menu';
import { cva, VariantProps } from 'class-variance-authority';

View file

@ -1,3 +1,5 @@
'use client';
import * as RDialog from '@radix-ui/react-dialog';
import { animated, useTransition } from '@react-spring/web';
import clsx from 'clsx';

View file

@ -1,3 +1,5 @@
'use client';
import { Menu, Transition } from '@headlessui/react';
import { ReactComponent as CaretDown } from '@sd/assets/svgs/caret.svg';
import { cva, VariantProps } from 'class-variance-authority';

View file

@ -1,3 +1,5 @@
'use client';
import * as RadixDM from '@radix-ui/react-dropdown-menu';
import clsx from 'clsx';
import React, {

View file

@ -1,3 +1,5 @@
'use client';
import { Eye, EyeSlash, Icon, IconProps, MagnifyingGlass } from '@phosphor-icons/react';
import { cva, VariantProps } from 'class-variance-authority';
import clsx from 'clsx';

View file

@ -1,3 +1,5 @@
'use client';
import * as Radix from '@radix-ui/react-popover';
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';

View file

@ -1,3 +1,5 @@
'use client';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import clsx from 'clsx';
import { memo } from 'react';

View file

@ -1,3 +1,5 @@
'use client';
/* eslint-disable tailwindcss/migration-from-tailwind-2 */
import * as RadioGroup from '@radix-ui/react-radio-group';
import clsx from 'clsx';

View file

@ -1,3 +1,5 @@
'use client';
import { Check } from '@phosphor-icons/react';
import * as RS from '@radix-ui/react-select';
import { ReactComponent as ChevronDouble } from '@sd/assets/svgs/chevron-double.svg';

View file

@ -1,3 +1,5 @@
'use client';
import * as SliderPrimitive from '@radix-ui/react-slider';
import clsx from 'clsx';

View file

@ -1,3 +1,5 @@
'use client';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { cva, VariantProps } from 'class-variance-authority';
import { forwardRef } from 'react';

View file

@ -1,3 +1,5 @@
'use client';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { tw } from './utils';

View file

@ -1,3 +1,5 @@
'use client';
import { CheckCircle, Icon, Info, Warning, WarningCircle, X } from '@phosphor-icons/react';
import clsx from 'clsx';
import { CSSProperties, ForwardedRef, forwardRef, ReactNode, useEffect, useState } from 'react';

View file

@ -1,3 +1,5 @@
'use client';
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import clsx from 'clsx';
import { PropsWithChildren, ReactNode } from 'react';

View file

@ -1,6 +1,3 @@
import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
import zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import clsx from 'clsx';
import { forwardRef, useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
@ -29,29 +26,48 @@ export interface PasswordInputProps extends UseFormFieldProps, Root.InputProps {
}
const PasswordStrengthMeter = (props: { password: string }) => {
const [zxcvbn, setZxcvbn] = useState<typeof import('./zxcvbn') | undefined>();
const [strength, setStrength] = useState<{ label: string; score: number }>();
const updateStrength = useDebouncedCallback(
() => setStrength(props.password ? getPasswordStrength(props.password) : undefined),
100
);
const updateStrength = useDebouncedCallback(() => {
if (!zxcvbn) return;
setStrength(props.password ? getPasswordStrength(props.password, zxcvbn) : undefined);
}, 100);
// TODO: Remove duplicate in @sd/client
function getPasswordStrength(password: string): { label: string; score: number } {
function getPasswordStrength(
password: string,
zxcvbn: typeof import('./zxcvbn')
): { label: string; score: number } {
const ratings = ['Poor', 'Weak', 'Good', 'Strong', 'Perfect'];
zxcvbnOptions.setOptions({
zxcvbn.zxcvbnOptions.setOptions({
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary
...zxcvbn.languageCommon.dictionary,
...zxcvbn.languageEn.dictionary
},
graphs: zxcvbnCommonPackage.adjacencyGraphs,
translations: zxcvbnEnPackage.translations
graphs: zxcvbn.languageCommon.adjacencyGraphs,
translations: zxcvbn.languageEn.translations
});
const result = zxcvbn(password);
const result = zxcvbn.zxcvbn(password);
return { label: ratings[result.score]!, score: result.score };
}
useEffect(() => {
let cancelled = false;
import('./zxcvbn').then((zxcvbn) => {
if (cancelled) return;
setZxcvbn(zxcvbn);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => updateStrength(), [props.password, updateStrength]);
return (

View file

@ -1,3 +1,5 @@
'use client';
export * from './Form';
export * from './FormField';
export * from './CheckBoxField';

View file

@ -0,0 +1,3 @@
export { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core';
export { default as languageCommon } from '@zxcvbn-ts/language-common';
export { default as languageEn } from '@zxcvbn-ts/language-en';

File diff suppressed because it is too large Load diff