mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
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:
parent
f352a28dc8
commit
d4ad5c97f3
|
@ -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';
|
||||
|
|
|
@ -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;
|
1
apps/landing/next-env.d.ts
vendored
1
apps/landing/next-env.d.ts
vendored
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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"
|
||||
|
|
69
apps/landing/src/app/Background.tsx
Normal file
69
apps/landing/src/app/Background.tsx
Normal 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>
|
||||
);
|
||||
}
|
211
apps/landing/src/app/Downloads.tsx
Normal file
211
apps/landing/src/app/Downloads.tsx
Normal 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>
|
||||
);
|
||||
}
|
158
apps/landing/src/app/Footer.tsx
Normal file
158
apps/landing/src/app/Footer.tsx
Normal 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">
|
||||
© 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>
|
||||
);
|
||||
}
|
32
apps/landing/src/app/HomeCTA.tsx
Normal file
32
apps/landing/src/app/HomeCTA.tsx
Normal 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;
|
78
apps/landing/src/app/NavBar/MobileDropdown.tsx
Normal file
78
apps/landing/src/app/NavBar/MobileDropdown.tsx
Normal 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')
|
||||
};
|
||||
}
|
74
apps/landing/src/app/NavBar/index.tsx
Normal file
74
apps/landing/src/app/NavBar/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
33
apps/landing/src/app/NewBanner.tsx
Normal file
33
apps/landing/src/app/NewBanner.tsx
Normal 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">→</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
8
apps/landing/src/app/Providers.tsx
Normal file
8
apps/landing/src/app/Providers.tsx
Normal 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>;
|
||||
}
|
78
apps/landing/src/app/blog/[slug]/page.tsx
Normal file
78
apps/landing/src/app/blog/[slug]/page.tsx
Normal 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> · {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>
|
||||
);
|
||||
}
|
54
apps/landing/src/app/blog/page.tsx
Normal file
54
apps/landing/src/app/blog/page.tsx
Normal 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} · {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>
|
||||
);
|
||||
}
|
82
apps/landing/src/app/careers/data.ts
Normal file
82
apps/landing/src/app/careers/data.ts
Normal 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'
|
||||
}
|
||||
];
|
137
apps/landing/src/app/careers/page.tsx
Normal file
137
apps/landing/src/app/careers/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
23
apps/landing/src/app/docs/[[...slug]]/Index.tsx
Normal file
23
apps/landing/src/app/docs/[[...slug]]/Index.tsx
Normal 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>
|
||||
);
|
||||
}
|
23
apps/landing/src/app/docs/[[...slug]]/Markdown.tsx
Normal file
23
apps/landing/src/app/docs/[[...slug]]/Markdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
15
apps/landing/src/app/docs/[[...slug]]/Search.tsx
Normal file
15
apps/landing/src/app/docs/[[...slug]]/Search.tsx
Normal 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>
|
||||
);
|
||||
}
|
81
apps/landing/src/app/docs/[[...slug]]/Sidebar.tsx
Normal file
81
apps/landing/src/app/docs/[[...slug]]/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
47
apps/landing/src/app/docs/[[...slug]]/data.ts
Normal file
47
apps/landing/src/app/docs/[[...slug]]/data.ts
Normal 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
|
||||
}))
|
||||
}))
|
||||
}));
|
67
apps/landing/src/app/docs/[[...slug]]/layout.tsx
Normal file
67
apps/landing/src/app/docs/[[...slug]]/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
81
apps/landing/src/app/docs/[[...slug]]/page.tsx
Normal file
81
apps/landing/src/app/docs/[[...slug]]/page.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
41
apps/landing/src/app/layout.tsx
Normal file
41
apps/landing/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
BIN
apps/landing/src/app/logo.png
Normal file
BIN
apps/landing/src/app/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 513 KiB |
40
apps/landing/src/app/not-found.tsx
Normal file
40
apps/landing/src/app/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
110
apps/landing/src/app/page.tsx
Normal file
110
apps/landing/src/app/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
13
apps/landing/src/app/pricing/Background.tsx
Normal file
13
apps/landing/src/app/pricing/Background.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -1,46 +1,18 @@
|
|||
'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>
|
||||
<PageWrapper>
|
||||
<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>
|
||||
<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" />
|
||||
<Switch checked={toggle} onCheckedChange={setToggle} size="lg" />
|
||||
<p className="text-sm font-medium text-white">Yearly</p>
|
||||
</div>
|
||||
<div
|
||||
|
@ -90,8 +62,6 @@ export default function PricingPage() {
|
|||
name="Enterprise"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
39
apps/landing/src/app/pricing/page.tsx
Normal file
39
apps/landing/src/app/pricing/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
114
apps/landing/src/app/roadmap/items.ts
Normal file
114
apps/landing/src/app/roadmap/items.ts
Normal 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.'
|
||||
}
|
||||
];
|
91
apps/landing/src/app/roadmap/page.tsx
Normal file
91
apps/landing/src/app/roadmap/page.tsx
Normal 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'
|
||||
)}
|
||||
>
|
||||
‍
|
||||
</div>
|
||||
) : (
|
||||
<div className="z-20">‍</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>
|
||||
);
|
||||
}
|
83
apps/landing/src/app/team/page.tsx
Normal file
83
apps/landing/src/app/team/page.tsx
Normal 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 legacy—that 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>
|
||||
);
|
||||
}
|
|
@ -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 legacy—that 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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import Image, { ImageProps } from 'next/image';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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> ·{' '}
|
||||
{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>
|
||||
);
|
||||
}
|
|
@ -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} ·{' '}
|
||||
{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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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'
|
||||
)}
|
||||
>
|
||||
‍
|
||||
</div>
|
||||
) : (
|
||||
<div className="z-20">‍</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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
})
|
||||
);
|
||||
}
|
|
@ -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()
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Preview } from '@storybook/react';
|
||||
|
||||
import '@sd/ui/style';
|
||||
import '@sd/ui/style.scss';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"gen": "node ./scripts/generate.mjs"
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "@sd/client",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import * as RadixDM from '@radix-ui/react-dropdown-menu';
|
||||
import clsx from 'clsx';
|
||||
import React, {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider';
|
||||
import clsx from 'clsx';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { tw } from './utils';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
'use client';
|
||||
|
||||
export * from './Form';
|
||||
export * from './FormField';
|
||||
export * from './CheckBoxField';
|
||||
|
|
3
packages/ui/src/forms/zxcvbn.ts
Normal file
3
packages/ui/src/forms/zxcvbn.ts
Normal 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';
|
689
pnpm-lock.yaml
689
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue