MAKE IT WORK BABBBYYYY

This commit is contained in:
logscore
2025-10-17 08:46:39 -06:00
parent 674b499cf9
commit c5defe4991
93 changed files with 596 additions and 1585 deletions

View File

@@ -1,15 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": true,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "origin/main",
"updateInternalDependencies": "patch",
"ignore": [],
"privatePackages": {
"version": true,
"tag": true
}
}

View File

@@ -1,226 +0,0 @@
name: Deploy Server, Migrate Database, and Deploy Web
run-name: Deploying to ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'staging' || 'preview' }} Environment by ${{ github.actor }}
on:
pull_request:
branches: [main]
types: [closed]
workflow_run:
workflows: ["Ready to Merge"]
types: [completed]
branches: [main, staging]
workflow_dispatch:
inputs:
deploy_server:
description: "Whether to deploy the server"
required: false
default: true
type: boolean
deploy_web:
description: "Whether to deploy the web application"
required: false
default: true
type: boolean
migrate_database:
description: "Whether to run database migrations"
required: false
default: true
type: boolean
create_release:
description: "Create Release"
required: false
default: true
type: boolean
pr_main:
description: "Create Pull Request to Main"
required: false
default: false
type: boolean
environment:
description: "Environment to deploy to"
required: true
default: "preview"
type: choice
options: ["production", "staging", "preview"]
env:
WORKFLOW_DEPLOY_ENV: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'staging' || 'preview' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
deploy:
if: ${{ (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success') && !startsWith(github.ref_name, 'release/') }}
runs-on: ubuntu-latest
timeout-minutes: 60
# Determine environment based on branch or input
environment:
name: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'staging' || 'preview' }}
steps:
- uses: actions/checkout@master
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check migrations
if: ${{ github.event.inputs.migrate_database != 'false' }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: cd packages/db && bun run check
- name: Download deployment info
if: ${{ github.event_name == 'workflow_run' && github.event.inputs.deploy_server != 'false' }}
uses: actions/download-artifact@master
with:
name: deployment-info
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Set deployment variables
if: ${{ github.event_name == 'workflow_run' && github.event.inputs.deploy_server != 'false' }}
run: |
if [ -f "deployment_name.txt" ]; then
DEPLOYMENT_NAME=$(cat deployment_name.txt)
echo "DEPLOY_COMMAND=deploy --env $WORKFLOW_DEPLOY_ENV --name $DEPLOYMENT_NAME" >> $GITHUB_ENV
else
echo "DEPLOY_COMMAND=deploy --env $WORKFLOW_DEPLOY_ENV" >> $GITHUB_ENV
fi
- name: Deploy Server Worker
if: ${{ github.event.inputs.deploy_server != 'false' }}
id: deploy-server-worker
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }}
MICROSOFT_CLIENT_SECRET: ${{ secrets.MICROSOFT_CLIENT_SECRET }}
BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
BETTER_AUTH_URL: ${{ secrets.BETTER_AUTH_URL }}
SERVER_PORT: ${{ secrets.SERVER_PORT }}
WEB_PORT: ${{ secrets.WEB_PORT }}
BACKEND_URL: ${{ secrets.BACKEND_URL }}
TRUSTED_ORIGINS: ${{ secrets.TRUSTED_ORIGINS }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_HOST: ${{ secrets.DATABASE_HOST }}
POSTGRES_PORT: ${{ secrets.POSTGRES_PORT }}
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
UPSTASH_REDIS_REST_URL: ${{ secrets.UPSTASH_REDIS_REST_URL }}
UPSTASH_REDIS_REST_TOKEN: ${{ secrets.UPSTASH_REDIS_REST_TOKEN }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
NODE_ENV: ${{ secrets.NODE_ENV }}
IS_EDGE_RUNTIME: ${{ secrets.IS_EDGE_RUNTIME }}
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
packageManager: bun
workingDirectory: "apps/server"
command: ${{ env.DEPLOY_COMMAND }}
# This is required for wrangler to know which environment to deploy to even though --env ${{ env.WORKFLOW_DEPLOY_ENV }} is specified in the command
environment: ${{ env.WORKFLOW_DEPLOY_ENV }}
secrets: |
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
MICROSOFT_CLIENT_ID
MICROSOFT_CLIENT_SECRET
BETTER_AUTH_SECRET
BETTER_AUTH_URL
SERVER_PORT
WEB_PORT
BACKEND_URL
TRUSTED_ORIGINS
DATABASE_URL
DATABASE_HOST
POSTGRES_PORT
POSTGRES_USER
POSTGRES_PASSWORD
POSTGRES_DB
UPSTASH_REDIS_REST_URL
UPSTASH_REDIS_REST_TOKEN
EMAIL_FROM
RESEND_API_KEY
NODE_ENV
IS_EDGE_RUNTIME
- name: Print Server deployment URL
if: ${{ github.event.inputs.deploy_server != 'false' }}
env:
SERVER_DEPLOYMENT_URL: ${{ steps.deploy-server-worker.outputs.deployment-url }}
run: echo $SERVER_DEPLOYMENT_URL
- name: Run migrations
if: ${{ github.event.inputs.migrate_database != 'false' }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: cd packages/db && bun run migrate
- name: Deploy Web Worker
if: ${{ github.event.inputs.deploy_web != 'false' }}
id: deploy-web-worker
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
NODE_ENV: ${{ secrets.NODE_ENV }}
VITE_BACKEND_URL: ${{ secrets.VITE_BACKEND_URL }}
VITE_FRONTEND_URL: ${{ secrets.VITE_FRONTEND_URL }}
run: |
cd apps/web
bun run cf:build
bun run cf:deploy:${{ env.WORKFLOW_DEPLOY_ENV }} | tee web-build.log
tail_output=$(tail -n 4 web-build.log)
rm web-build.log
echo "$tail_output"
echo "tail_output<<EOF" >> $GITHUB_OUTPUT
echo "$tail_output" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Print Web deployment output
if: ${{ github.event.inputs.deploy_web != 'false' }}
env:
WEB_DEPLOYMENT_OUTPUT: ${{ steps.deploy-web-worker.outputs.tail_output }}
run: echo $WEB_DEPLOYMENT_OUTPUT
- name: Setup Git
if: ${{ github.ref_name == 'staging' && github.event.inputs.create_release != 'false' }}
run: |
git config --global user.name "${{ github.actor || 'GitHub Actions' }}"
git config --global user.email "${{ github.actor || 'github-actions' }}@users.noreply.github.com"
- name: Create Release
if: ${{ github.ref_name == 'staging' && github.event.inputs.create_release != 'false' }}
id: create-release
run: ./scripts/release.sh
- name: Create Pull Request to Main
if: ${{ github.ref_name == 'staging' && github.event.inputs.pr_main != 'false' }}
id: create-pr-to-main
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: main
title: "Release: ${{ github.sha }}"
body: >-
Automated release PR from staging branch
- Deployment URL:
${{ steps.deploy-server-worker.outputs.deployment-url }}
- Web Deployment Output:
${{ steps.deploy-web-worker.outputs.tail_output }}
This PR is created automatically after successful staging deployment.
labels: release, automated

View File

@@ -1,124 +0,0 @@
name: CI
run-name: "Branch: ${{ github.ref_name }}. Event: ${{ github.event_name }}. By: ${{ github.actor }}."
on:
push:
branches: [main, staging]
pull_request:
branches: [main, staging]
jobs:
ci:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: nimbus-test
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5
minio:
image: minio/minio:edge-cicd
ports:
- 9000:9000
env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
MINIO_REGION: us-east-1
options: >-
--health-cmd "curl -f http://localhost:9000/minio/health/ready" --health-interval 30s --health-timeout 10s --health-retries 3
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@v4
with:
node-version: latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run formatter
run: bun format
- name: Run linter
run: bun lint
- name: Run tests with coverage
run: bun run test:coverage
- name: Build application
env:
NODE_ENV: production
DATABASE_URL: postgres://postgres:postgres@localhost:5432/nimbus-test
VITE_BACKEND_URL: http://localhost:1284
VITE_FRONTEND_URL: http://localhost:3000
run: bun run build
- name: Create coverage badge
if: github.ref == 'refs/heads/staging'
run: |
# Extract coverage percentage from JSON summary
if [ -f "coverage/coverage-summary.json" ]; then
coverage=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync('coverage/coverage-summary.json')); const total=data.total; if(total && total.lines && total.lines.pct !== undefined) { console.log(total.lines.pct); } else { console.log('0'); }")
else
coverage=0
fi
# If coverage extraction fails, default to 0
if [ -z "$coverage" ] || [ "$coverage" = "null" ]; then coverage=0; fi
# Round coverage to integer
coverage=$(printf "%.0f" "$coverage")
# Determine color based on coverage
if [ "$coverage" -ge 80 ]; then
color=green
elif [ "$coverage" -ge 50 ]; then
color=yellow
else
color=red
fi
# Create a simpler badge using shields.io style URL
echo "Coverage: $coverage%"
continue-on-error: true
- name: Commit coverage report
if: github.ref == 'refs/heads/staging'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
# Update README with actual coverage percentage using shields.io badge
if [ -f "coverage/coverage-summary.json" ]; then
coverage=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync('coverage/coverage-summary.json')); const total=data.total; if(total && total.lines && total.lines.pct !== undefined) { console.log(total.lines.pct); } else { console.log('0'); }")
else
coverage=0
fi
if [ -z "$coverage" ] || [ "$coverage" = "null" ]; then coverage=0; fi
coverage=$(printf "%.0f" "$coverage")
if [ "$coverage" -ge 80 ]; then
color=green
elif [ "$coverage" -ge 50 ]; then
color=yellow
else
color=red
fi
sed -i "s/coverage-[0-9]*%25-[a-z]*/coverage-$coverage%25-$color/" README.md
git add README.md
git commit -m "Update coverage badge [skip ci]" || echo "No changes to commit"
git push
continue-on-error: true

View File

@@ -1,5 +1,3 @@
"use client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { forgotPasswordSchema, type ForgotPasswordFormData } from "@nimbus/shared";
import { useForm, type SubmitHandler } from "react-hook-form";
@@ -14,7 +12,7 @@ import { Input } from "@/components/ui/input";
import type { ComponentProps } from "react";
export function ForgotPasswordForm({ ...props }: ComponentProps<"div">) {
const { isLoading, forgotPassword } = useForgotPassword();
const { mutate, isPending } = useForgotPassword();
const {
register,
@@ -28,7 +26,7 @@ export function ForgotPasswordForm({ ...props }: ComponentProps<"div">) {
});
const onSubmit: SubmitHandler<ForgotPasswordFormData> = async data => {
await forgotPassword(data);
mutate(data);
};
return (
@@ -69,8 +67,8 @@ export function ForgotPasswordForm({ ...props }: ComponentProps<"div">) {
<FieldError error={errors.email?.message} />
</div>
<Button type="submit" className="mt-2 w-full cursor-pointer" disabled={isLoading}>
{isLoading ? (
<Button type="submit" className="mt-2 w-full cursor-pointer" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending reset link...

View File

@@ -1,5 +1,3 @@
"use client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { resetPasswordSchema, type ResetPasswordFormData } from "@nimbus/shared";
import { AuthErrorCard } from "@/components/auth/shared/auth-error-card";
@@ -7,20 +5,20 @@ import { ArrowLeft, Eye, EyeClosed, Loader2 } from "lucide-react";
import { useForm, type SubmitHandler } from "react-hook-form";
import { FieldError } from "@/components/ui/field-error";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import { useResetPassword } from "@/hooks/useAuth";
import { useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import type { ComponentProps } from "react";
import { useState } from "react";
import Link from "next/link";
export function ResetPasswordForm({ ...props }: ComponentProps<"div">) {
const searchParams = useSearchParams();
const error = searchParams.get("error");
const token = searchParams.get("token");
const { isLoading, resetPassword } = useResetPassword();
const searchParams = useSearch({ from: "/_public/reset-password" });
const error = searchParams.error;
const token = searchParams.token;
const { mutate, isPending } = useResetPassword();
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const {
@@ -39,7 +37,7 @@ export function ResetPasswordForm({ ...props }: ComponentProps<"div">) {
if (!token) {
throw new Error("Reset token is missing");
}
await resetPassword(data, token);
await mutate(data, token);
};
if (error === "invalid_token" || !token) {
@@ -59,7 +57,7 @@ export function ResetPasswordForm({ ...props }: ComponentProps<"div">) {
<CardHeader className="overflow-x-hidden">
<div className="-mx-6 flex flex-row items-center justify-start border-b">
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href="/">
<Link to="/">
<ArrowLeft />
Back
</Link>
@@ -115,8 +113,8 @@ export function ResetPasswordForm({ ...props }: ComponentProps<"div">) {
<FieldError error={errors.confirmPassword?.message as string} />
</div>
<Button type="submit" className="mt-2 w-full cursor-pointer" disabled={isLoading}>
{isLoading ? (
<Button type="submit" className="mt-2 w-full cursor-pointer" disabled={isPending}>
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Resetting password...
@@ -131,7 +129,7 @@ export function ResetPasswordForm({ ...props }: ComponentProps<"div">) {
<CardFooter className="px-6 py-4">
<p className="w-full text-center text-sm text-neutral-600">
By continuing, you agree to our{" "}
<Link href="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
<Link to="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
terms of service
</Link>
.

View File

@@ -1,13 +1,11 @@
"use client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowLeft, ArrowRight } from "lucide-react";
import type { AuthCardProps } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import Link from "next/link";
export function AuthCard({ title, description, navigationType, children, className, ...props }: AuthCardProps) {
export function AuthCard({ title, navigationType, children, className, ...props }: AuthCardProps) {
const oppositeAction = navigationType === "signin" ? "signup" : "signin";
const oppositeActionText = navigationType === "signin" ? "Sign up" : "Sign in";
@@ -17,13 +15,13 @@ export function AuthCard({ title, description, navigationType, children, classNa
<CardHeader className="overflow-x-hidden">
<div className="-mx-6 flex flex-row items-center justify-between border-b">
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href="/">
<Link to="/">
<ArrowLeft />
Back
</Link>
</Button>
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href={`/${oppositeAction}`}>
<Link to={`/${oppositeAction}`}>
{oppositeActionText}
<ArrowRight />
</Link>
@@ -31,7 +29,6 @@ export function AuthCard({ title, description, navigationType, children, classNa
</div>
<div className="gap-2 pt-6">
<CardTitle className="text-center text-lg md:text-xl">{title}</CardTitle>
<CardDescription className="text-center text-xs md:text-sm">{description}</CardDescription>
</div>
</CardHeader>
@@ -40,7 +37,7 @@ export function AuthCard({ title, description, navigationType, children, classNa
<CardFooter className="px-6 py-4">
<p className="w-full text-center text-sm text-neutral-600">
By {navigationType === "signin" ? "signing in" : "signing up"}, you agree to our{" "}
<Link href="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
<Link to="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
terms of service
</Link>
.

View File

@@ -1,7 +1,7 @@
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
interface AuthErrorCardProps {
title: string;
@@ -18,7 +18,7 @@ export function AuthErrorCard({ title, content, actionText, actionHref, classNam
<CardHeader className="overflow-x-hidden">
<div className="-mx-6 flex flex-row items-center justify-start border-b">
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href="/">
<Link to="/">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Link>
@@ -35,7 +35,7 @@ export function AuthErrorCard({ title, content, actionText, actionHref, classNam
<CardFooter className="px-6 py-4">
<Button asChild className="w-full">
<Link href={actionHref}>{actionText}</Link>
<Link to={actionHref}>{actionText}</Link>
</Button>
</CardFooter>
</Card>

View File

@@ -1,142 +1,34 @@
"use client";
import { useGoogleAuth, useMicrosoftAuth } from "@/hooks/useAuth";
import { SocialAuthButton } from "./social-auth-button";
import type { DriveProvider } from "@nimbus/shared";
// import { Button } from "@/components/ui/button";
import { useSocialAuth } from "@/hooks/useAuth";
import { Loader2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
type AuthProviderButtonsProps = {
onProviderClick?: (provider: DriveProvider) => Promise<void> | void;
isLoading?: boolean | Record<DriveProvider, boolean>;
action: "signin" | "signup";
showS3Button?: boolean;
callbackURL?: string;
onAuthSuccess?: () => void;
onS3Click?: () => void;
};
export function AuthProviderButtons({
onProviderClick,
isLoading: externalIsLoading,
action,
showS3Button = false,
callbackURL,
onAuthSuccess,
onS3Click,
}: AuthProviderButtonsProps) {
// Use external loading state or manage internal loading state
const [internalIsLoading, setInternalIsLoading] = useState<Record<DriveProvider, boolean>>({
google: false,
microsoft: false,
box: false,
dropbox: false,
s3: false,
});
const isLoading = externalIsLoading || internalIsLoading;
const getIsLoading = (provider: DriveProvider) => {
return typeof isLoading === "boolean" ? isLoading : isLoading[provider];
};
const { signInWithGoogleProvider } = useGoogleAuth();
const { signInWithMicrosoftProvider } = useMicrosoftAuth();
// const { signInWithBoxProvider } = useBoxAuth();
// const { signInWithDropboxProvider } = useDropboxAuth();
const handleSocialAuth = async (provider: Exclude<DriveProvider, "s3">) => {
try {
// If external handler is provided, use it
if (onProviderClick) {
return await onProviderClick(provider);
}
// Otherwise, handle internally
setInternalIsLoading(prev => ({ ...prev, [provider]: true }));
if (provider === "google") {
await signInWithGoogleProvider({ callbackURL });
} else if (provider === "microsoft") {
await signInWithMicrosoftProvider({ callbackURL });
// } else if (provider === "box") {
// await signInWithBoxProvider({ callbackURL });
// } else if (provider === "dropbox") {
// await signInWithDropboxProvider({ callbackURL });
}
onAuthSuccess?.();
} catch (error) {
console.error(`Error signing in with ${provider}:`, error);
toast.error(`${provider.charAt(0).toUpperCase() + provider.slice(1)} authentication failed`);
} finally {
setInternalIsLoading(prev => ({ ...prev, [provider]: false }));
}
};
const handleProviderClick = async (provider: DriveProvider) => {
if (provider === "s3") {
if (onProviderClick) {
await onProviderClick("s3");
} else if (onS3Click) {
onS3Click();
}
} else {
await handleSocialAuth(provider);
}
};
export function AuthProviderButtons({ action, callbackURL }: AuthProviderButtonsProps) {
const socialAuthMutation = useSocialAuth();
const isLoading = socialAuthMutation.isPending;
return (
<>
<SocialAuthButton
provider="google"
action={action}
onClick={() => handleProviderClick("google")}
disabled={getIsLoading("google")}
>
{getIsLoading("google") && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</SocialAuthButton>
onClick={() => socialAuthMutation.mutate({ provider: "google", callbackURL })}
disabled={isLoading}
className={`{isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
></SocialAuthButton>
<SocialAuthButton
provider="microsoft"
action={action}
onClick={() => handleProviderClick("microsoft")}
disabled={getIsLoading("microsoft")}
>
{getIsLoading("microsoft") && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</SocialAuthButton>
{/*
<SocialAuthButton
provider="box"
action={action}
onClick={() => handleProviderClick("box")}
disabled={getIsLoading("box")}
>
{getIsLoading("box") && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</SocialAuthButton>
<SocialAuthButton
provider="dropbox"
action="signin"
onClick={() => handleSocialAuth("dropbox")}
disabled={getIsLoading("dropbox")}
>
{getIsLoading("dropbox") && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</SocialAuthButton>
{showS3Button && (
<Button
variant="outline"
onClick={() => handleProviderClick("s3")}
disabled={getIsLoading("s3")}
className="flex items-center gap-2"
>
<Cloud className="h-4 w-4" />
Amazon S3 / S3-Compatible
</Button>
)} */}
onClick={() => socialAuthMutation.mutate({ provider: "microsoft", callbackURL })}
disabled={isLoading}
className={`{isLoading ? "opacity-50 cursor-not-allowed" : ""}`}
></SocialAuthButton>
</>
);
}

View File

@@ -1,5 +1,3 @@
"use client";
import type { PasswordInputProps } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

View File

@@ -1,8 +1,7 @@
"use client";
import { BoxIcon, DropboxIcon, GoogleIcon, MicrosoftIcon } from "@/components/icons";
import type { SocialAuthButtonProps } from "@/lib/types";
import { Button } from "@/components/ui/button";
import type { PropsWithChildren } from "react";
const providerConfig = {
google: {
@@ -23,12 +22,7 @@ const providerConfig = {
},
} as const;
export function SocialAuthButton({
provider,
action,
children,
...props
}: React.PropsWithChildren<SocialAuthButtonProps>) {
export function SocialAuthButton({ provider, action, children, ...props }: PropsWithChildren<SocialAuthButtonProps>) {
const config = providerConfig[provider];
const IconComponent = config.icon;
@@ -40,7 +34,7 @@ export function SocialAuthButton({
<Button
variant="outline"
type="button"
className="w-full cursor-pointer justify-between truncate shadow-md shadow-blue-600/20 transition-all duration-300 hover:shadow-sm hover:shadow-blue-600/20 dark:shadow-lg"
className="w-full cursor-pointer justify-between truncate shadow-md transition-all duration-300 hover:shadow-sm"
{...props}
>
<IconComponent />

View File

@@ -1,11 +1,9 @@
"use client";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { AuthProviderButtons } from "@/components/auth/shared/auth-provider-buttons";
import { S3AccountForm } from "@/components/settings/s3-account-form";
import { useLocation } from "@tanstack/react-router";
import { useIsMounted } from "@/hooks/useIsMounted";
import type { DriveProvider } from "@nimbus/shared";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
type SigninAccountDialogProps = {
@@ -17,7 +15,9 @@ type ViewMode = "select" | "s3-form";
export function SigninAccountDialog({ open, onOpenChange }: SigninAccountDialogProps) {
const isMounted = useIsMounted();
const pathname = usePathname();
const pathname = useLocation({
select: location => location.pathname,
});
const [callbackURL, setCallbackURL] = useState<string>("");
const [viewMode, setViewMode] = useState<ViewMode>("select");
const [isLoading] = useState<Record<DriveProvider, boolean>>({

View File

@@ -1,5 +1,3 @@
"use client";
import { AuthProviderButtons } from "@/components/auth/shared/auth-provider-buttons";
import { PasswordInput } from "@/components/auth/shared/password-input";
import { signInSchema, type SignInFormData } from "@nimbus/shared";
@@ -10,14 +8,15 @@ import type { ChangeEvent, ComponentProps } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useSignIn } from "@/hooks/useAuth";
import { Loader2 } from "lucide-react";
import Link from "next/link";
export function SignInForm({ className, ...props }: ComponentProps<"div">) {
const { signInWithCredentials, isLoading } = useSignIn();
const signInMutation = useSignIn();
const isLoading = signInMutation.isPending;
const {
register,
@@ -37,19 +36,13 @@ export function SignInForm({ className, ...props }: ComponentProps<"div">) {
const passwordValue = watch("password");
const onSubmit: SubmitHandler<SignInFormData> = async data => {
await signInWithCredentials(data);
signInMutation.mutate(data);
};
return (
<AuthCard
title="Welcome back to Nimbus.storage"
description="You do the files, we store them."
navigationType="signin"
className={className}
{...props}
>
<AuthCard title="Welcome back to Nimbus" navigationType="signin" className={className} {...props}>
<div className="flex flex-col gap-4">
<AuthProviderButtons action="signin" isLoading={isLoading} />
<AuthProviderButtons action="signin" />
<div className="text-muted-foreground text-center font-mono text-sm font-semibold tracking-wider">OR</div>
@@ -101,7 +94,7 @@ export function SignInForm({ className, ...props }: ComponentProps<"div">) {
</Label>
</div>
<Link
href="/forgot-password"
to="/forgot-password"
className="text-muted-foreground hover:text-primary text-sm underline underline-offset-4 transition-colors"
>
Forgot password?

View File

@@ -1,5 +1,3 @@
"use client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { AuthProviderButtons } from "@/components/auth/shared/auth-provider-buttons";
import { SegmentedProgress } from "@/components/ui/segmented-progress";
@@ -10,21 +8,19 @@ import { FieldError } from "@/components/ui/field-error";
import { Separator } from "@/components/ui/separator";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, type ComponentProps } from "react";
import { useSearchParams } from "next/navigation";
import { useSearch } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { useForm } from "react-hook-form";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import Link from "next/link";
export function SignupForm({ className, ...props }: ComponentProps<"div">) {
const searchParams = useSearchParams();
const urlEmail = searchParams.get("email");
const searchParams = useSearch({ from: "/_public/signup" });
const [showPasswordEntry, setShowPasswordEntry] = useState(false);
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const { isLoading, signUpWithCredentials } = useSignUp();
const signUpMutation = useSignUp();
const checkEmailMutation = useCheckEmailExists();
const {
@@ -37,7 +33,7 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
} = useForm<SignUpFormData>({
resolver: zodResolver(signUpSchema),
defaultValues: {
email: urlEmail ?? "",
email: "",
firstName: "",
lastName: "",
password: "",
@@ -49,32 +45,24 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
const isValid = await trigger(["firstName", "lastName", "email"]);
if (isValid) {
const email = getValues("email");
// Check email exists is now handled by the mutation's onSuccess/onError
checkEmailMutation.mutate(email, {
onSuccess: data => {
if (data.exists) {
setError("email", {
type: "manual",
message: "An account with this email already exists. Please sign in instead.",
message: "An account with this email already exists. Please sign in to continue.",
});
toast.error("An account with this email already exists. Please sign in instead.");
} else {
setShowPasswordEntry(true);
}
},
onError: () => {
toast.error("Failed to verify email. Please try again.");
},
});
}
};
const handleGoBack = () => {
setShowPasswordEntry(false);
};
const onSubmit = async (data: SignUpFormData) => {
await signUpWithCredentials(data);
signUpMutation.mutate(data);
};
return (
@@ -83,7 +71,7 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
<CardHeader className="overflow-x-hidden">
<div className="-mx-6 flex flex-row items-center justify-start border-b">
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href={`/signin`}>
<Link to={`/signin`}>
<ArrowLeft className="mr-2" />
Sign in
</Link>
@@ -99,10 +87,19 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
</CardHeader>
<CardContent className="px-6">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4"
onKeyDown={e => {
if (e.key === "Enter" && !showPasswordEntry) {
e.preventDefault();
handleContinue();
}
}}
>
{!showPasswordEntry && (
<>
<AuthProviderButtons action="signup" isLoading={isLoading} />
<AuthProviderButtons action="signup" />
<div className="text-muted-foreground text-center font-mono text-sm font-semibold tracking-wider">
OR
</div>
@@ -121,7 +118,7 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
{...register("firstName")}
aria-invalid={!!errors.firstName}
/>
<FieldError error={errors.firstName?.message as string} />
<FieldError error={errors.firstName?.message} />
</div>
<div className="space-y-1">
<Label htmlFor="lastName" className="dark:text-muted-foreground text-sm font-semibold">
@@ -134,7 +131,7 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
{...register("lastName")}
aria-invalid={!!errors.lastName}
/>
<FieldError error={errors.lastName?.message as string} />
<FieldError error={errors.lastName?.message} />
</div>
</div>
@@ -146,11 +143,10 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
id="email"
type="email"
placeholder="example@0.email"
className=""
{...register("email")}
aria-invalid={!!errors.email}
/>
<FieldError error={errors.email?.message as string} />
<FieldError error={errors.email?.message} />
</div>
{!showPasswordEntry && (
@@ -158,15 +154,13 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
type="button"
className="w-full cursor-pointer font-semibold"
onClick={handleContinue}
disabled={isLoading || checkEmailMutation.isPending}
disabled={checkEmailMutation.isPending}
>
{checkEmailMutation.isPending ? (
<>
<Loader2 className="mr-2 animate-spin" />
Checking email...
</>
) : isLoading ? (
<Loader2 className="animate-spin" />
) : (
"Continue"
)}
@@ -196,8 +190,9 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
placeholder="Enter your password"
{...register("password")}
aria-invalid={!!errors.password}
disabled={signUpMutation.isPending}
/>
<FieldError error={errors.password?.message as string} />
<FieldError error={errors.password?.message} />
</div>
<div className="space-y-1">
@@ -210,12 +205,13 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
placeholder="Confirm your password"
{...register("confirmPassword")}
aria-invalid={!!errors.confirmPassword}
disabled={signUpMutation.isPending}
/>
<FieldError error={errors.confirmPassword?.message as string} />
<FieldError error={errors.confirmPassword?.message} />
</div>
<Button type="submit" className="flex-1" disabled={isLoading}>
{isLoading ? <Loader2 className="animate-spin" /> : "Create Account"}
<Button type="submit" className="flex-1" disabled={signUpMutation.isPending}>
{signUpMutation.isPending ? <Loader2 className="animate-spin" /> : "Create Account"}
</Button>
</div>
)}
@@ -224,7 +220,7 @@ export function SignupForm({ className, ...props }: ComponentProps<"div">) {
<CardFooter className="px-6 py-4">
<p className="w-full text-center text-sm text-neutral-600">
By signing up, you agree to our{" "}
<Link href="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
<Link to="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
terms of service
</Link>
.

View File

@@ -1,101 +1,101 @@
"use client";
// import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
// import { AuthErrorCard } from "./shared/auth-error-card";
import { type ComponentProps } from "react";
// import { useSearchParams } from "next/navigation";
// import { ArrowLeft, Loader2 } from "lucide-react";
// import { Button } from "@/components/ui/button";
// import { Link } from "@tanstack/react-router";
// import axios, { AxiosError } from "axios";
// import env from "@nimbus/env/client";
// import { toast } from "sonner";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { AuthErrorCard } from "./shared/auth-error-card";
import { type ComponentProps, useState } from "react";
import { useSearchParams } from "next/navigation";
import { ArrowLeft, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import axios, { AxiosError } from "axios";
import env from "@nimbus/env/client";
import { toast } from "sonner";
import Link from "next/link";
export function VerifyEmailContent({ ...props }: ComponentProps<"div">) {}
// TODO: This stuff
export function VerifyEmailContent({ ...props }: ComponentProps<"div">) {
const searchParams = useSearchParams();
const error = searchParams.get("error");
const token = searchParams.get("token");
const [isLoading, setIsLoading] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const callbackURL = searchParams.get("callbackURL");
// const searchParams = useSearchParams();
// const error = searchParams.get("error");
// const token = searchParams.get("token");
// const [isLoading, setIsLoading] = useState(false);
// const [isVerified, setIsVerified] = useState(false);
// const callbackURL = searchParams.get("callbackURL");
const onClick = async () => {
if (!token) return;
setIsLoading(true);
try {
await axios.get(`${import.meta.env.VITE_BACKEND_URL}/api/auth/verify-email`, {
withCredentials: true,
params: {
token,
callbackURL,
},
});
onSuccess();
} catch (error) {
if (!(error instanceof AxiosError)) {
console.error(error);
toast.error("Failed to verify email. Please try again.");
} else {
onSuccess();
}
} finally {
setIsLoading(false);
}
};
// const onClick = async () => {
// if (!token) return;
// setIsLoading(true);
// try {
// client
// withCredentials: true,
// params: {
// token,
// callbackURL,
// },
// });
// onSuccess();
// } catch (error) {
// if (!(error instanceof AxiosError)) {
// console.error(error);
// toast.error("Failed to verify email. Please try again.");
// } else {
// onSuccess();
// }
// } finally {
// setIsLoading(false);
// }
// };
const onSuccess = () => {
setIsVerified(true);
toast.success("Email verified successfully");
};
// const onSuccess = () => {
// setIsVerified(true);
// toast.success("Email verified successfully");
// };
if (error === "invalid_token" || !token) {
return (
<AuthErrorCard
title="Invalid Email Change Link"
content="This email change link is invalid or has expired. Please request a new email change link to continue."
actionText="Request to Change Email"
actionHref="/dashboard/settings"
/>
);
}
// if (error === "invalid_token" || !token) {
// return (
// <AuthErrorCard
// title="Invalid Email Change Link"
// content="This email change link is invalid or has expired. Please request a new email change link to continue."
// actionText="Request to Change Email"
// actionHref="/dashboard/settings"
// />
// );
// }
return (
<div className="flex size-full flex-col items-center justify-center gap-0 select-none" {...props}>
<Card className="w-full max-w-md gap-6 py-0 pb-0">
<CardHeader className="overflow-x-hidden">
<div className="flex flex-row items-center justify-start border-b">
<Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
<Link href="/">
<ArrowLeft />
Back to Home
</Link>
</Button>
</div>
<div className="gap-2 pt-6">
<CardTitle className="text-center text-lg md:text-xl">Verify Email</CardTitle>
<CardDescription className="text-center text-xs md:text-sm">
Click the button below to verify your email.
</CardDescription>
</div>
</CardHeader>
// return (
// <div className="flex size-full flex-col items-center justify-center gap-0 select-none" {...props}>
// <Card className="w-full max-w-md gap-6 py-0 pb-0">
// <CardHeader className="overflow-x-hidden">
// <div className="flex flex-row items-center justify-start border-b">
// <Button className="cursor-pointer rounded-none px-6 py-6 font-semibold" variant="link" asChild>
// <Link to="/">
// <ArrowLeft />
// Back to Home
// </Link>
// </Button>
// </div>
// <div className="gap-2 pt-6">
// <CardTitle className="text-center text-lg md:text-xl">Verify Email</CardTitle>
// <CardDescription className="text-center text-xs md:text-sm">
// Click the button below to verify your email.
// </CardDescription>
// </div>
// </CardHeader>
<CardContent className="flex items-center justify-center px-6">
<Button onClick={onClick} disabled={isLoading || isVerified}>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{isVerified ? "Email Verified" : "Verify Email"}
</Button>
</CardContent>
// <CardContent className="flex items-center justify-center px-6">
// <Button onClick={onClick} disabled={isLoading || isVerified}>
// {isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
// {isVerified ? "Email Verified" : "Verify Email"}
// </Button>
// </CardContent>
<CardFooter className="px-6 py-4">
<p className="w-full text-center text-sm text-neutral-600">
By continuing, you agree to our{" "}
<Link href="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
terms of service
</Link>
.
</p>
</CardFooter>
</Card>
</div>
);
}
// <CardFooter className="px-6 py-4">
// <p className="w-full text-center text-sm text-neutral-600">
// By continuing, you agree to our{" "}
// <Link to="/terms" className="cursor-pointer whitespace-nowrap underline underline-offset-4">
// terms of service
// </Link>
// .
// </p>
// </CardFooter>
// </Card>
// </div>
// );
// }

View File

@@ -1,8 +1,8 @@
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Card, CardTitle } from "@/components/ui/card";
import { Link } from "@tanstack/react-router";
import { Badge } from "@/components/ui/badge";
import { GitCommit } from "lucide-react";
import Link from "next/link";
export interface Contributor {
login: string;
@@ -16,7 +16,7 @@ export interface Contributor {
export function ContributorCard({ contributor }: { contributor: Contributor }) {
return (
<Link href={contributor.html_url} target="_blank" rel="noopener noreferrer" className="block max-w-full">
<Link to={contributor.html_url} target="_blank" rel="noopener noreferrer" className="block max-w-full">
<Card className="group flex aspect-square w-full flex-col rounded-sm border-dashed p-3 transition-all duration-200 hover:cursor-pointer hover:border-solid hover:shadow-md">
<div className="flex flex-1 flex-col items-center justify-center space-y-2 text-center">
<Avatar className="h-12 w-12 rounded-lg border-0 sm:h-14 sm:w-14 md:h-16 md:w-16">

View File

@@ -1,5 +1,3 @@
"use client";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Area, AreaChart, XAxis } from "recharts";

View File

@@ -1,6 +1,6 @@
import { Card, CardContent } from "@/components/ui/card";
import { Star, GitFork, Calendar } from "lucide-react";
import Link from "next/link";
import { Link } from "@tanstack/react-router";
export function ContributorFooter({
repoName,
@@ -22,7 +22,7 @@ export function ContributorFooter({
</div>
<div className="flex flex-wrap items-center justify-center gap-4 text-sm">
<Link
href={repoUrl}
to={repoUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary inline-flex items-center gap-2 font-medium hover:underline"
@@ -32,7 +32,7 @@ export function ContributorFooter({
</Link>
<span className="text-muted-foreground"></span>
<Link
href={repoUrl}
to={repoUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary inline-flex items-center gap-2 font-medium hover:underline"

View File

@@ -1,12 +1,10 @@
"use client";
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from "@/components/ui/drawer";
import { CircleAlert, CircleCheckBig, CloudUpload, RefreshCcw } from "lucide-react";
import { uploadMutationKey, useUploadFile } from "@/hooks/useFileOperations";
import { useMutationState, type MutationState } from "@tanstack/react-query";
import { MAX_FILE_SIZE, type UploadFileSchema } from "@nimbus/shared";
import { AnimatePresence, motion } from "motion/react";
import { useSearchParams } from "next/navigation";
import { useSearch } from "@tanstack/react-router";
import { useDropzone } from "react-dropzone";
import { getModernFileIcon } from ".";
import { cn } from "@/lib/utils";
@@ -18,8 +16,8 @@ export default function DragNDropUploader({ children }: { children: React.ReactN
const [openDialog, setOpenDialog] = useState(false);
const searchParams = useSearchParams();
const parentId = searchParams.get("folderId") ?? "root";
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const parentId = searchParams.folderId ?? "root";
const { getRootProps, isDragActive, acceptedFiles } = useDropzone({
multiple: true,

View File

@@ -1,19 +1,20 @@
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
export function FilePreview() {
const router = useRouter();
const searchParams = useSearchParams();
const id = searchParams.get("id");
const navigate = useNavigate({ from: "/dashboard/$providerSlug/$accountId" });
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const handleClose = () => {
const params = new URLSearchParams(searchParams.toString());
params.delete("id");
router.replace(`?${params.toString()}`);
const { id, ...restSearchParams } = searchParams;
navigate({
search: restSearchParams,
replace: true,
});
};
return (
<Sheet open={!!id} onOpenChange={open => !open && handleClose()}>
<Sheet open={!!searchParams.id} onOpenChange={open => !open && handleClose()}>
<SheetContent>
<div className="p-4">
<p>File preview is being revamped. Please check back later.</p>

View File

@@ -1,48 +0,0 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useRouter, useSearchParams } from "next/navigation";
export function FileTabs({ type }: { type: string | null }) {
const router = useRouter();
const searchParams = useSearchParams();
const handleTabChange = (value: string) => {
const params = new URLSearchParams(searchParams);
if (value === "all") {
params.delete("type");
} else {
params.set("type", value);
}
router.push(`?${params.toString()}`);
};
return (
<Tabs value={type ?? "all"} onValueChange={handleTabChange} className="w-[400px]">
<TabsList className="bg-muted/50 grid w-full grid-cols-4 rounded-lg p-1">
<TabsTrigger
value="all"
className="data-[state=active]:bg-background data-[state=active]:text-foreground text-sm transition-all data-[state=active]:shadow-sm"
>
All
</TabsTrigger>
<TabsTrigger
value="folder"
className="data-[state=active]:bg-background data-[state=active]:text-foreground text-sm transition-all data-[state=active]:shadow-sm"
>
Folders
</TabsTrigger>
<TabsTrigger
value="document"
className="data-[state=active]:bg-background data-[state=active]:text-foreground text-sm transition-all data-[state=active]:shadow-sm"
>
Documents
</TabsTrigger>
<TabsTrigger
value="media"
className="data-[state=active]:bg-background data-[state=active]:text-foreground text-sm transition-all data-[state=active]:shadow-sm"
>
Media
</TabsTrigger>
</TabsList>
</Tabs>
);
}

View File

@@ -1,5 +1,3 @@
"use client";
import {
Archive,
ChevronDown,
@@ -25,12 +23,12 @@ import {
type SortingState,
} from "@tanstack/react-table";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useRouter, useSearchParams } from "next/navigation";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useDraggable, useDroppable } from "@dnd-kit/react";
import React, { useMemo, useState, type JSX } from "react";
import { UploadButton } from "@/components/upload-button";
import { pointerIntersection } from "@dnd-kit/collision";
import DragNDropUploader from "./drag-n-drop-uploader";
import { useMemo, useState, type JSX } from "react";
import { Button } from "@/components/ui/button";
import { formatFileSize } from "@nimbus/shared";
import { PdfIcon } from "@/components/icons";
@@ -79,73 +77,70 @@ const columnHelper = createColumnHelper<File>();
export function FileTable({ files, isLoading, refetch, error }: FileTableProps) {
const { tags } = useTags(files[0]?.parentId);
const router = useRouter();
const searchParams = useSearchParams();
const navigate = useNavigate({ from: "/dashboard/$providerSlug/$accountId" });
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const [sorting, setSorting] = useState<SortingState>([]);
const [filteredFiles, setFilteredFiles] = useState<File[]>([]);
const searchType = searchParams.get("type");
const safeFiles = useMemo(() => {
if (isLoading || !files || !Array.isArray(files)) {
return [];
}
return files;
}, [files, isLoading]);
let result = [...files];
const filteredFiles = useMemo(() => {
if (!safeFiles.length) return [];
// Apply type filter if specified
if (searchType) {
result = result.filter(file => {
const mimeType = file.mimeType?.toLowerCase() ?? "";
const fileName = file.name?.toLowerCase() ?? "";
const searchType = searchParams.type;
if (!searchType) return safeFiles;
switch (searchType) {
case "folder":
return mimeType === "application/vnd.google-apps.folder" || mimeType === "folder";
case "document":
return (
// Google Docs
mimeType.includes("application/vnd.google-apps.document") ||
mimeType.includes("application/vnd.google-apps.spreadsheet") ||
mimeType.includes("application/vnd.google-apps.presentation") ||
// Microsoft Office
mimeType.includes("officedocument") ||
mimeType.includes("msword") ||
// PDFs
mimeType.includes("pdf") ||
// Text files
mimeType.includes("text/") ||
// Common document extensions
/\.(doc|docx|xls|xlsx|ppt|pptx|pdf|txt|rtf|odt|ods|odp)$/i.test(fileName)
);
case "media":
return (
// Images
mimeType.includes("image/") ||
// Videos
mimeType.includes("video/") ||
// Audio
mimeType.includes("audio/") ||
// Common media extensions
/\.(jpg|jpeg|png|gif|bmp|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(fileName)
);
default:
return true;
}
});
}
return safeFiles.filter(file => {
const mimeType = file.mimeType?.toLowerCase() ?? "";
const fileName = file.name?.toLowerCase() ?? "";
setFilteredFiles(result);
return result;
}, [files, isLoading, searchType]);
switch (searchType) {
case "folder":
return mimeType === "application/vnd.google-apps.folder" || mimeType === "folder";
case "document":
return (
// Google Docs
mimeType.includes("application/vnd.google-apps.document") ||
mimeType.includes("application/vnd.google-apps.spreadsheet") ||
mimeType.includes("application/vnd.google-apps.presentation") ||
// Microsoft Office
mimeType.includes("officedocument") ||
mimeType.includes("msword") ||
// PDFs
mimeType.includes("pdf") ||
// Text files
mimeType.includes("text/") ||
// Common document extensions
/\.(doc|docx|xls|xlsx|ppt|pptx|pdf|txt|rtf|odt|ods|odp)$/i.test(fileName)
);
case "media":
return (
// Images
mimeType.includes("image/") ||
// Videos
mimeType.includes("video/") ||
// Audio
mimeType.includes("audio/") ||
// Common media extensions
/\.(jpg|jpeg|png|gif|bmp|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(fileName)
);
default:
return true;
}
});
}, [safeFiles, searchParams.type]);
const handleRowDoubleClick = (file: File): void => {
const fileType = file.mimeType.includes("folder") || file.mimeType === "folder" ? "folder" : "file";
if (fileType === "folder") {
const params = new URLSearchParams(searchParams);
params.set("folderId", file.id);
router.push(`?${params.toString()}`);
navigate({
search: { ...searchParams, folderId: file.id },
});
}
};
@@ -277,7 +272,7 @@ export function FileTable({ files, isLoading, refetch, error }: FileTableProps)
);
const table = useReactTable({
data: searchType ? filteredFiles : safeFiles,
data: searchParams.type ? filteredFiles : safeFiles,
columns,
state: {
sorting,

View File

@@ -1,9 +1,7 @@
"use client";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from "@/components/ui/breadcrumb";
import { type BreadcrumbItem as BreadcrumbItemType } from "@/hooks/useBreadcrumb";
import { AnimatePresence, motion, type Variants } from "framer-motion";
import { useRouter, useSearchParams } from "next/navigation";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useBreadcrumbPath } from "@/hooks/useBreadcrumb";
import { pointerIntersection } from "@dnd-kit/collision";
import { SourceSelector } from "./source-selector";
@@ -34,25 +32,26 @@ const variants: Variants = {
};
export function FileBreadcrumb() {
const searchParams = useSearchParams();
const router = useRouter();
const currentFileId = searchParams.get("folderId") || "";
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const navigate = useNavigate({ from: "/dashboard/$providerSlug/$accountId" });
const currentFileId = searchParams.folderId || "";
const { data } = useBreadcrumbPath(currentFileId);
// Handle clicking a folder navigation
async function handleFolderClick(id: string) {
const params = new URLSearchParams(searchParams);
params.set("folderId", id);
router.push(`?${params.toString()}`);
function handleFolderClick(id: string) {
navigate({
search: { ...searchParams, folderId: id },
});
}
// Handle clicking the home icon to remove folderId
async function handleHomeClick() {
const params = new URLSearchParams(searchParams);
const folderId = params.get("folderId");
if (!folderId) return;
params.delete("folderId");
router.push(`?${params.toString()}`);
function handleHomeClick() {
if (!searchParams.folderId) return;
const { folderId, ...restSearchParams } = searchParams;
navigate({
search: restSearchParams,
});
}
const { ref: droppableRef, isDropTarget } = useDroppable({

View File

@@ -1,5 +1,3 @@
"use client";
import { type ComponentProps } from "react";
import { Sidebar, SidebarContent, SidebarHeader } from "@/components/ui/sidebar";

View File

@@ -1,5 +1,3 @@
"use client";
import {
SidebarGroup,
SidebarGroupContent,
@@ -8,10 +6,10 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useNavigate, useSearch, useParams } from "@tanstack/react-router";
import { ChevronDown, FileText, Folder, PinOff } from "lucide-react";
import { providerToSlug, type DriveProvider } from "@nimbus/shared";
import { usePinnedFiles, useUnpinFile } from "@/hooks/useDriveOps";
import { useRouter, useSearchParams } from "next/navigation";
import { Skeleton } from "@/components/ui/skeleton";
import type { PinnedFile } from "@nimbus/shared";
import { Button } from "@/components/ui/button";
@@ -29,8 +27,8 @@ function getFileIcon(type: string) {
}
export default function SidebarPinnedFiles() {
const searchParams = useSearchParams();
const router = useRouter();
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const navigate = useNavigate({ from: "/dashboard/$providerSlug/$accountId" });
const [isOpen, setIsOpen] = useState(true);
const { data: pinnedFiles, isLoading, error } = usePinnedFiles();
const unpinFile = useUnpinFile();
@@ -40,22 +38,21 @@ export default function SidebarPinnedFiles() {
};
const handleNavigate = (file: PinnedFile) => {
const params = new URLSearchParams(searchParams.toString());
if (file.type === "folder") {
params.set("folderId", file.fileId);
} else {
return;
navigate({
to: "/dashboard/$providerSlug/$accountId",
params: {
providerSlug: providerToSlug(file.provider as DriveProvider),
accountId: file.accountId,
},
search: { ...searchParams, folderId: file.fileId },
});
}
const isValidProvider = (provider: string): provider is DriveProvider => {
return provider === "microsoft" || provider === "google";
};
// If not a folder, we don't navigate
};
if (!isValidProvider(file.provider)) {
console.error(`Invalid provider: ${file.provider}`);
return;
}
router.push(`/dashboard/${providerToSlug(file.provider)}/${file.accountId}?${params.toString()}`);
const isValidProvider = (provider: string): provider is DriveProvider => {
return provider === "microsoft" || provider === "google";
};
return (

View File

@@ -1,15 +1,13 @@
"use client";
import { SidebarFooter, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import { Progress } from "@/components/ui/progress";
import { useDriveInfo } from "@/hooks/useDriveOps";
import { Moon, Settings, Sun } from "lucide-react";
// import { Button } from "@/components/ui/button";
import { formatFileSize } from "@nimbus/shared";
import { Link } from "@tanstack/react-router";
import { useTheme } from "@/hooks/useTheme";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import Link from "next/link";
export default function StorageFooter() {
const { data, error, isError, isPending } = useDriveInfo();
@@ -81,7 +79,7 @@ export default function StorageFooter() {
asChild
className="transition-all duration-200 ease-linear hover:bg-neutral-200 dark:hover:bg-neutral-700"
>
<Link href="/dashboard/settings">
<Link to="/dashboard/settings">
<Settings className="size-4" />
<span>Settings</span>
</Link>

View File

@@ -1,4 +1,3 @@
"use client";
import {
SidebarGroup,
SidebarGroupContent,

View File

@@ -1,5 +1,3 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,

View File

@@ -1,5 +1,3 @@
"use client";
import {
Dialog,
DialogContent,

View File

@@ -1,5 +1,3 @@
"use client";
import {
Dialog,
DialogContent,

View File

@@ -1,5 +1,3 @@
"use client";
import {
Dialog,
DialogContent,

View File

@@ -1,4 +1,4 @@
import Link from "next/link";
import { Link } from "@tanstack/react-router";
const WarningBanner = () => {
return (
@@ -7,7 +7,7 @@ const WarningBanner = () => {
<div className="text-sm">
This project is not currently maintained. If you like what the project is and would like to see it continue,
reach out on our{" "}
<Link href="https://discord.gg/c9nWy26ubK" className="font-semibold underline">
<Link to="https://discord.gg/c9nWy26ubK" className="font-semibold underline">
Discord
</Link>
</div>

View File

@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
import { LogoIcon } from "@/components/icons";
import Link from "next/link";
// this is a copy of Analogs footer component with some changes
// https://github.com/analogdotnow/Analog/blob/main/apps/web/src/components/footer.tsx
@@ -10,10 +10,10 @@ export default function Footer() {
<div className="mx-auto flex w-full max-w-7xl flex-row items-center justify-center">
<div className="text-muted-foreground flex flex-row items-center justify-center gap-2">
<LogoIcon className="h-9 w-9" aria-hidden="true" />
<Link href="/terms" className="text-xs underline underline-offset-2 md:text-sm">
<Link to="/terms" className="text-xs underline underline-offset-2 md:text-sm">
Terms of Use
</Link>
<Link href="/privacy" className="text-xs underline underline-offset-2 md:text-sm">
<Link to="/privacy" className="text-xs underline underline-offset-2 md:text-sm">
Privacy Policy
</Link>
</div>

View File

@@ -1,17 +1,15 @@
"use client";
import { DiscordIcon, GitHubIcon, LogoIcon, XPlatformIcon } from "@/components/icons";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ModeToggle } from "@/components/mode-toggle";
import { Button } from "@/components/ui/button";
import { Link } from "@tanstack/react-router";
import { Users } from "lucide-react";
import Link from "next/link";
export default function Header() {
return (
<header className="border-border bg-surface/80 fixed top-4 left-1/2 z-50 mx-auto flex w-full max-w-xs -translate-x-1/2 items-center justify-between rounded-lg border px-4 py-2 backdrop-blur-xs md:max-w-2xl">
<h1>
<Link href="/" className="hover:text-primary/80 flex items-center gap-2 font-bold transition-colors">
<Link to="/" className="hover:text-primary/80 flex items-center gap-2 font-bold transition-colors">
<span>
<LogoIcon className="h-9 w-9" aria-hidden="true" />
</span>
@@ -22,7 +20,7 @@ export default function Header() {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" asChild aria-label="Contributors" className="h-9 w-9">
<Link href="/contributors">
<Link to="/contributors">
<Users className="h-5 w-5" />
</Link>
</Button>
@@ -32,9 +30,9 @@ export default function Header() {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" aria-label="Discord" className="h-9 w-9">
<a href="https://discord.gg/c9nWy26ubK" target="_blank" rel="noopener noreferrer">
<Link href="https://discord.gg/c9nWy26ubK" target="_blank" rel="noopener noreferrer">
<DiscordIcon />
</a>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Discord</TooltipContent>
@@ -42,9 +40,9 @@ export default function Header() {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" asChild className="h-9 w-9">
<a href="https://github.com/nimbusdotstorage/Nimbus" target="_blank" rel="noopener noreferrer">
<Link href="https://github.com/nimbusdotstorage/Nimbus" target="_blank" rel="noopener noreferrer">
<GitHubIcon />
</a>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>Github</TooltipContent>
@@ -52,9 +50,9 @@ export default function Header() {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" aria-label="X (Twitter)" className="h-9 w-9">
<a href="https://x.com/nimbusdotcloud" target="_blank" rel="noopener noreferrer">
<Link href="https://x.com/nimbusdotcloud" target="_blank" rel="noopener noreferrer">
<XPlatformIcon />
</a>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent>X (Twitter)</TooltipContent>

View File

@@ -33,22 +33,22 @@ export default function Hero() {
<Header />
<AnimatedGroup variants={transitionVariants} className="w-full">
<div className="relative flex w-full flex-col gap-12 px-4 md:px-6">
{isMobile && (
{/*{isMobile && (
<BgAngels className="pointer-events-none absolute -top-40 left-40 z-0 h-auto rotate-12 opacity-50" />
)}
)}*/}
<div className="relative mx-auto w-full max-w-3xl sm:max-w-4xl md:max-w-5xl lg:max-w-6xl">
<div className="pointer-events-none absolute top-1/2 left-1/2 z-0 block h-[60vw] w-[120vw] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.7)_60%,rgba(255,255,255,0.2)_100%)] blur-[100px] sm:h-[80%] sm:w-[120%] dark:hidden" />
<div className="pointer-events-none absolute top-1/2 left-1/2 z-0 hidden h-[60vw] w-[120vw] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(ellipse_at_center,rgba(10,10,20,0.7)_60%,rgba(10,10,20,0.2)_100%)] blur-[100px] sm:h-[80%] sm:w-[120%] dark:block" />
<div className="absolute bottom-[-100%] left-[-100px] z-0 hidden sm:block">
{/*<div className="absolute bottom-[-100%] left-[-100px] z-0 hidden sm:block">
<BgAngels className="scale-x-[-1] -rotate-12 opacity-40" alt="angel right" />
</div>
</div>*/}
<div className="absolute right-[-100px] bottom-[-100%] z-0 hidden sm:block">
{/*<div className="absolute right-[-100px] bottom-[-100%] z-0 hidden sm:block">
<BgAngels className="rotate-12 opacity-40" alt="angel left" />
</div>
</div>*/}
<div className="relative z-10 flex flex-col items-center justify-center gap-8 text-center md:gap-12 lg:gap-12">
<h1 className="text-4xl leading-[1.1] font-bold tracking-[-0.02em] sm:flex-row md:text-6xl lg:text-7xl">

View File

@@ -1,5 +1,3 @@
"use client";
import { emailObjectSchema, type ApiResponse, type WaitlistCount } from "@nimbus/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";

View File

@@ -1,5 +1,3 @@
"use client";
import { Loader2 } from "lucide-react";
import { useEffect } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,5 +1,3 @@
"use client";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { Moon, Sun } from "lucide-react";

View File

@@ -1,13 +1,9 @@
import {
type DriveProvider,
type DriveProviderSlug,
type DriveProviderSlugParam,
providerToSlug,
slugToProvider,
} from "@nimbus/shared";
import { type DriveProvider, type DriveProviderSlug, providerToSlug, slugToProvider } from "@nimbus/shared";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { createProtectedClient, type DriveProviderClient } from "@/utils/client";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useNavigate } from "@tanstack/react-router";
import { useLocation } from "@tanstack/react-router";
import { useParams } from "@tanstack/react-router";
interface AccountProviderContextType {
providerId: string | null;
@@ -33,9 +29,11 @@ const createClient = (providerId: string | null, accountId: string | null): Prom
const AccountProviderContext = createContext<AccountProviderContextType | null>(null);
export function AccountProvider({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const { providerSlug: providerSlugParam, accountId: accountIdParam } = useParams<DriveProviderSlugParam>();
const navigate = useNavigate();
const location = useLocation();
const { providerSlug: providerSlugParam, accountId: accountIdParam } = useParams({
from: "/_protected/dashboard/$providerSlug/$accountId",
});
const [providerSlug, setProviderSlug] = useState<string | null>(providerSlugParam);
const [providerId, setProviderId] = useState<string | null>(
@@ -73,11 +71,14 @@ export function AccountProvider({ children }: { children: React.ReactNode }) {
const navigateToProvider = useCallback(
(newProviderSlug: string, newAccountId: string) => {
const newPathname = `/dashboard/${newProviderSlug}/${newAccountId}`;
if (pathname !== newPathname) {
router.push(newPathname);
if (location.pathname !== newPathname) {
navigate({
to: "/dashboard/$providerSlug/$accountId",
params: { providerSlug: newProviderSlug, accountId: newAccountId },
});
}
},
[pathname, router]
[location.pathname, navigate]
);
const setDriveProviderById = useCallback(

View File

@@ -1,5 +1,3 @@
"use client";
import { SigninAccountDialog } from "@/components/auth/signin-account-dialog";
import { AuthProvider, useAuth } from "@/components/providers/auth-provider";
import { setAuthContext } from "@/utils/client";

View File

@@ -1,5 +1,3 @@
"use client";
import { createContext, useContext, useState, useCallback } from "react";
type AuthContextType = {

View File

@@ -1,5 +1,3 @@
"use client";
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import type { DriveProvider, DriveProviderSlug } from "@nimbus/shared";
import { useNavigate, useLocation } from "@tanstack/react-router";

View File

@@ -1,5 +1,3 @@
"use client";
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
import { DownloadProgress } from "@/components/ui/download-progress";

View File

@@ -1,4 +1,3 @@
"use client";
import { authClient } from "@nimbus/auth/auth-client";
import { PostHogProvider } from "posthog-js/react";
import { type ReactNode, useEffect } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { RouteGuard } from "./route-guard";
interface ProtectedRouteProps {

View File

@@ -1,5 +1,3 @@
"use client";
import { RouteGuard } from "./route-guard";
interface PublicRouteProps {

View File

@@ -1,5 +1,3 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";

View File

@@ -1,7 +1,5 @@
"use client";
import { useNavigate, useLocation } from "@tanstack/react-router";
import { authClient } from "@nimbus/auth/auth-client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
interface RouteGuardProps {
@@ -11,7 +9,8 @@ interface RouteGuardProps {
}
export function RouteGuard({ children, requireAuth = false, redirectTo = "/signin" }: RouteGuardProps) {
const router = useRouter();
const navigate = useNavigate();
const location = useLocation();
const { data: session, isPending } = authClient.useSession();
useEffect(() => {
@@ -21,15 +20,16 @@ export function RouteGuard({ children, requireAuth = false, redirectTo = "/signi
if (requireAuth && !isAuthenticated) {
// Redirect to signin if auth is required but user isn't authenticated
const currentPath = window.location.pathname;
const redirectUrl = `${redirectTo}?redirect=${encodeURIComponent(currentPath)}`;
router.push(redirectUrl);
} else if (!requireAuth && isAuthenticated && window.location.pathname === "/signin") {
navigate({
to: redirectTo,
// search: { redirect: location.pathname },
});
} else if (!requireAuth && isAuthenticated && location.pathname === "/signin") {
// Redirect to dashboard if already signed in and on signin page
router.push("/dashboard");
navigate({ to: "/dashboard" });
}
}
}, [session, isPending, requireAuth, redirectTo, router]);
}, [session, isPending, requireAuth, redirectTo, location.pathname, navigate]);
// Show loading state while checking auth
if (isPending) {

View File

@@ -1,5 +1,3 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ComponentProps } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import type { LimitedAccessAccount } from "@nimbus/shared";
import { createContext, useContext, useMemo } from "react";
import type { SessionUser } from "@nimbus/auth/auth";

View File

@@ -1,5 +1,3 @@
"use client";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { FileText, Filter, Folder, Search, Tag } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";

View File

@@ -1,13 +1,18 @@
import { useNavigate } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { ArrowLeft } from "lucide-react";
export function SettingsHeader() {
const router = useRouter();
const navigate = useNavigate();
const handleBack = () => {
navigate({ to: ".." });
};
return (
<header className="bg-background flex h-16 items-center border-b p-4">
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" className="h-9 w-9" onClick={() => router.push("/dashboard")}>
<Button variant="ghost" size="icon" className="h-9 w-9" onClick={handleBack}>
<ArrowLeft className="h-5 w-5" />
<span className="sr-only">Back</span>
</Button>

View File

@@ -1,5 +1,3 @@
"use client";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AWS_REGIONS, createS3AccountSchema, type CreateS3AccountSchema } from "@nimbus/shared";
import { FieldError } from "@/components/ui/field-error";

View File

@@ -1,4 +1,3 @@
"use client";
import type { ReactNode, JSX, ElementType } from "react";
import { motion, type Variants } from "motion/react";
import { Children, useMemo } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Fallback, Image, Root } from "@radix-ui/react-avatar";
import type { ComponentProps } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Root, Indicator } from "@radix-ui/react-checkbox";
import type { ComponentProps } from "react";
import { CheckIcon } from "lucide-react";

View File

@@ -1,5 +1,3 @@
"use client";
import {
Root,
CollapsibleTrigger as CollapsibleTriggerPrimitive,

View File

@@ -1,5 +1,3 @@
"use client";
import { Close, Content, Description, Overlay, Portal, Root, Title, Trigger } from "@radix-ui/react-dialog";
import { ArrowLeftIcon, XIcon } from "lucide-react";

View File

@@ -1,5 +1,3 @@
"use client";
import { CheckCircle, Loader2, XCircle } from "lucide-react";
import { Progress } from "@/components/ui/progress";
import { useEffect, useState } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Drawer as DrawerPrimitive } from "vaul";
import * as React from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import {
Root,
Portal,

View File

@@ -1,5 +1,3 @@
"use client";
import { Root } from "@radix-ui/react-label";
import type { ComponentProps } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Indicator, Root } from "@radix-ui/react-progress";
import type { ComponentProps } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Corner, Root, ScrollAreaScrollbar, ScrollAreaThumb, Viewport } from "@radix-ui/react-scroll-area";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,5 +1,3 @@
"use client";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Root } from "@radix-ui/react-separator";
import type { ComponentProps } from "react";

View File

@@ -1,5 +1,3 @@
"use client";
import { Description, Root, Trigger, Close, Portal, Overlay, Title, Content } from "@radix-ui/react-dialog";
import type { ComponentProps } from "react";
import { XIcon } from "lucide-react";

View File

@@ -1,5 +1,3 @@
"use client";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";

View File

@@ -1,5 +1,3 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,5 +1,3 @@
"use client";
import { Root, List, Trigger, Content } from "@radix-ui/react-tabs";
import type { ComponentProps } from "react";

View File

@@ -1,4 +1,3 @@
"use client";
import { motion, AnimatePresence, type Transition, type Variants, type AnimatePresenceProps } from "motion/react";
import { useState, useEffect, Children, type ReactNode } from "react";
import { cn } from "@/lib/utils";

View File

@@ -1,5 +1,3 @@
"use client";
import { Provider, Root, Trigger, Content, Portal, Arrow } from "@radix-ui/react-tooltip";
import type { ComponentProps } from "react";

View File

@@ -1,4 +1,3 @@
"use client";
import {
DropdownMenu,
DropdownMenuContent,
@@ -9,7 +8,7 @@ import { UploadFolderDialog } from "@/components/dialogs/upload-folder-dialog";
import { CreateFolderDialog } from "@/components/dialogs/create-folder-dialog";
import { UploadFileDialog } from "@/components/dialogs/upload-files-dialog";
import { FolderPlus, Plus, Upload } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useSearch } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { useState } from "react";
@@ -17,8 +16,8 @@ export function UploadButton({ name }: { name: string }) {
const [uploadFileOpen, setUploadFileOpen] = useState(false);
const [uploadFolderOpen, setUploadFolderOpen] = useState(false);
const [createFolderOpen, setCreateFolderOpen] = useState(false);
const searchParams = useSearchParams();
const folderId = searchParams.get("folderId") ?? "root";
const searchParams = useSearch({ from: "/_protected/dashboard/$providerSlug/$accountId" });
const folderId = searchParams.folderId ?? "root";
return (
<>

View File

@@ -1,5 +1,3 @@
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cva, type VariantProps } from "class-variance-authority";
import { useIsMounted } from "@/hooks/useIsMounted";

View File

@@ -7,37 +7,14 @@ import {
type SignInFormData,
type SignUpFormData,
} from "@nimbus/shared";
import { useSearchParamsSafely } from "@/hooks/useSearchParamsSafely";
import { authClient } from "@nimbus/auth/auth-client";
import { useNavigate } from "@tanstack/react-router";
import { useMutation } from "@tanstack/react-query";
import { publicClient } from "@/utils/client";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import env from "@nimbus/env/client";
import { toast } from "sonner";
interface AuthState {
isLoading: boolean;
error: string | null;
}
const signInWithProvider = async (provider: DriveProvider) => {
return authClient.signIn.social({
provider,
callbackURL: `${import.meta.env.VITE_FRONTEND_URL}/dashboard`,
});
};
const linkSessionWithProvider = async (
provider: DriveProvider,
callbackURL: string = `${import.meta.env.VITE_FRONTEND_URL}/dashboard`
) => {
return authClient.linkSocial({
provider,
callbackURL,
});
};
// Simple error handler
const handleAuthError = (error: unknown, defaultMessage: string): string => {
if (error instanceof Error) {
return error.message || defaultMessage;
@@ -45,334 +22,186 @@ const handleAuthError = (error: unknown, defaultMessage: string): string => {
return defaultMessage;
};
const getProviderDisplayName = (provider: DriveProvider): string => {
return provider.charAt(0).toUpperCase() + provider.slice(1);
};
// Social auth hook
export const useSocialAuth = () => {
const navigate = useNavigate();
const useSocialAuth = (provider: DriveProvider) => {
const [isLoading, setIsLoading] = useState(false);
const providerName = getProviderDisplayName(provider);
return useMutation({
mutationFn: async (options: { provider: DriveProvider; callbackURL?: string }) => {
const isLoggedIn = await authClient.getSession();
const action = isLoggedIn.data?.session ? "link" : "signin";
const providerName = options.provider.charAt(0).toUpperCase() + options.provider.slice(1);
const handleAuth = useCallback(
async (options?: { callbackURL?: string }) => {
setIsLoading(true);
const authPromise =
action === "link"
? authClient.linkSocial({
provider: options.provider,
callbackURL: options.callbackURL || `${env.VITE_FRONTEND_URL}/dashboard`,
})
: authClient.signIn.social({
provider: options.provider,
callbackURL: `${env.VITE_FRONTEND_URL}/dashboard`,
});
try {
const isLoggedIn = await authClient.getSession();
// If the user is already logged in, link the provider
const action = isLoggedIn.data?.session ? "link" : "signin";
const authPromise =
action === "link" ? linkSessionWithProvider(provider, options?.callbackURL) : signInWithProvider(provider);
toast.promise(authPromise, {
loading: action === "link" ? `Linking ${providerName} account...` : `Signing in with ${providerName}...`,
success: action === "link" ? `Successfully linked ${providerName} account` : `Signed in with ${providerName}`,
error: (error: unknown) => handleAuthError(error, `${providerName} authentication failed`),
});
return true;
} catch (error) {
const errorMessage = handleAuthError(error, `${providerName} authentication failed`);
toast.error(errorMessage);
return false;
} finally {
setIsLoading(false);
}
return toast.promise(authPromise, {
loading: action === "link" ? `Linking ${providerName} account...` : `Signing in with ${providerName}...`,
success: action === "link" ? `Successfully linked ${providerName} account` : `Signed in with ${providerName}`,
error: error => handleAuthError(error, `${options.provider} authentication failed`),
});
},
[provider, providerName]
);
return { handleAuth, isLoading };
};
export const useGoogleAuth = () => {
const { handleAuth, isLoading } = useSocialAuth("google");
return {
signInWithGoogleProvider: handleAuth,
isLoading,
};
};
export const useMicrosoftAuth = () => {
const { handleAuth, isLoading } = useSocialAuth("microsoft");
return {
signInWithMicrosoftProvider: handleAuth,
isLoading,
};
};
export const useBoxAuth = () => {
const { handleAuth, isLoading } = useSocialAuth("box");
return {
signInWithBoxProvider: handleAuth,
isLoading,
};
};
export const useDropboxAuth = () => {
const { handleAuth, isLoading } = useSocialAuth("dropbox");
return {
signInWithDropboxProvider: handleAuth,
isLoading,
};
};
const useRedirect = () => {
const router = useRouter();
const { getParam } = useSearchParamsSafely();
const getRedirectUrl = useCallback(() => {
return getParam("redirect") || "/dashboard";
}, [getParam]);
const redirectToDashboard = useCallback(() => {
const redirectUrl = getRedirectUrl();
router.push(redirectUrl);
router.refresh();
}, [router, getRedirectUrl]);
return { getRedirectUrl, redirectToDashboard };
};
const useAuthMutation = <TData, TResult = unknown>(mutationFn: (data: TData) => Promise<TResult>) => {
const [state, setState] = useState<AuthState>({ isLoading: false, error: null });
const mutate = useCallback(
async (data: TData, options: Parameters<typeof toast.promise<TResult>>[1]) => {
setState({ isLoading: true, error: null });
try {
toast.promise(mutationFn(data), options);
} catch (error) {
const errorMessage = handleAuthError(error, "An unexpected error occurred.");
setState(prev => ({ ...prev, error: errorMessage }));
throw error; // Re-throw for form-level error handling
} finally {
setState(prev => ({ ...prev, isLoading: false }));
}
onSuccess: () => {
navigate({ to: "/dashboard" });
},
[mutationFn]
);
return { ...state, mutate };
});
};
// Sign in hook
export const useSignIn = () => {
const { redirectToDashboard } = useRedirect();
const navigate = useNavigate();
const signInMutation = useCallback(
(data: SignInFormData) =>
authClient.signIn.email(
return useMutation({
mutationFn: async (data: SignInFormData) => {
await authClient.signIn.email(
{
email: data.email,
password: data.password,
rememberMe: data.remember,
},
{
onSuccess: redirectToDashboard,
onSuccess: () => {
const redirectUrl = new URLSearchParams(window.location.search).get("redirect") || "/dashboard";
navigate({ to: redirectUrl });
},
onError: ctx => {
throw ctx.error;
},
}
),
[redirectToDashboard]
);
const { mutate, ...state } = useAuthMutation(signInMutation);
const signInWithCredentials = (data: SignInFormData) =>
mutate(data, {
loading: "Signing you in...",
success: `Welcome back, ${data.email}!`,
error: error => handleAuthError(error, "Unable to sign in. Please try again."),
});
return { ...state, signInWithCredentials };
);
return data.email;
},
onSuccess: email => {
toast.success(`Welcome back, ${email}!`);
},
onError: error => {
toast.error(handleAuthError(error, "Unable to sign in. Please try again."));
},
});
};
// Sign up hook
export const useSignUp = () => {
const { redirectToDashboard } = useRedirect();
const navigate = useNavigate();
const signUpMutation = useCallback(
async (data: SignUpFormData) => {
return useMutation({
mutationFn: async (data: SignUpFormData) => {
const fullName = `${data.firstName} ${data.lastName}`;
await authClient.signUp.email({
name: fullName,
email: data.email,
password: data.password,
callbackURL: `${import.meta.env.VITE_FRONTEND_URL}/dashboard`,
callbackURL: `${env.VITE_FRONTEND_URL}/dashboard`,
});
redirectToDashboard();
return fullName;
},
[redirectToDashboard]
);
const { mutate, ...state } = useAuthMutation(signUpMutation);
const signUpWithCredentials = (data: SignUpFormData) => {
const fullName = `${data.firstName} ${data.lastName}`;
return mutate(data, {
loading: "Creating your account...",
success: `Welcome to Nimbus, ${fullName}!`,
error: error => {
if (error instanceof Error) {
if (error.message.toLowerCase().includes("exists")) {
return "An account with this email already exists. Please sign in instead.";
} else if (error.message.toLowerCase().includes("password")) {
return "Password doesn't meet requirements. Please check and try again.";
}
return error.message;
onSuccess: fullName => {
navigate({ to: "/dashboard" });
},
onError: error => {
let errorMessage = "Unable to create your account. Please try again.";
if (error instanceof Error) {
if (error.message.toLowerCase().includes("exists")) {
errorMessage = "An account with this email already exists. Please sign in instead.";
} else if (error.message.toLowerCase().includes("password")) {
errorMessage = "Password doesn't meet requirements. Please check and try again.";
} else {
errorMessage = error.message;
}
return "Unable to create your account. Please try again.";
},
});
};
return { ...state, signUpWithCredentials };
};
export const useSignOut = () => {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const signOut = useCallback(
async (options?: { redirectTo?: string }) => {
setIsLoading(true);
try {
const response = await authClient.signOut();
const data = response.data;
const success = data?.success ?? false;
if (!success) {
throw new Error("Sign out failed");
}
toast.success("Signed out successfully");
// Redirect to the specified path or default to signin
const redirectPath = options?.redirectTo || "/signin";
router.push(redirectPath);
router.refresh();
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Sign out failed";
toast.error(errorMessage);
return { success: false, error: errorMessage };
} finally {
setIsLoading(false);
}
toast.error(errorMessage);
},
[router]
);
return {
signOut,
isLoading,
};
};
const checkEmailExists = async (email: string): Promise<CheckEmailExists> => {
try {
const body = {
email,
};
const result = emailObjectSchema.safeParse(body);
if (!result.success) {
throw new Error(result.error.message);
}
const response = await publicClient.api.auth["check-email"].$post({ json: body });
return (await response.json()) as CheckEmailExists;
} catch (error) {
if (error instanceof Error) {
throw new Error(error.message || "Failed to check email existence");
}
throw error;
}
};
export const useCheckEmailExists = () => {
return useMutation<CheckEmailExists, Error, string>({
mutationFn: checkEmailExists,
});
};
export const useForgotPassword = () => {
const [state, setState] = useState<AuthState>({ isLoading: false, error: null });
// Sign out hook
export const useSignOut = () => {
const navigate = useNavigate();
const forgotPassword = useCallback(async (data: ForgotPasswordFormData) => {
setState({ isLoading: true, error: null });
return useMutation({
mutationFn: async (options?: { redirectTo?: string }) => {
const response = await authClient.signOut();
const data = response.data;
const success = data?.success ?? false;
try {
toast.promise(
authClient.forgetPassword({
email: data.email,
redirectTo: `${window.location.origin}/reset-password`,
}),
{
loading: "Sending password reset email...",
success: "If an account exists with this email, you will receive a password reset link.",
error: error => handleAuthError(error, "Failed to send password reset email. Please try again."),
}
);
return true;
} catch (error) {
const errorMessage = handleAuthError(error, "Failed to send password reset email.");
setState({ isLoading: false, error: errorMessage });
throw error;
} finally {
setState(prev => ({ ...prev, isLoading: false }));
}
}, []);
if (!success) {
throw new Error("Sign out failed");
}
return { ...state, forgotPassword };
return { success: true, redirectPath: options?.redirectTo || "/signin" };
},
onSuccess: ({ redirectPath }) => {
toast.success("Signed out successfully");
navigate({ to: redirectPath });
},
onError: error => {
toast.error(handleAuthError(error, "Sign out failed"));
},
});
};
export const useResetPassword = () => {
const router = useRouter();
const [state, setState] = useState<AuthState>({ isLoading: false, error: null });
// Check email exists hook
export const useCheckEmailExists = () => {
return useMutation<CheckEmailExists, Error, string>({
mutationFn: async (email: string) => {
const body = { email };
const result = emailObjectSchema.safeParse(body);
if (!result.success) {
throw new Error(result.error.message);
}
const response = await publicClient.api.auth["check-email"].$post({ json: body });
return (await response.json()) as CheckEmailExists;
},
});
};
const resetPassword = useCallback(
async (data: ResetPasswordFormData, token: string) => {
// Forgot password hook
export const useForgotPassword = () => {
return useMutation({
mutationFn: async (data: ForgotPasswordFormData) => {
await authClient.forgetPassword({
email: data.email,
redirectTo: `${window.location.origin}/reset-password`,
});
},
onSuccess: () => {
toast.success("If an account exists with this email, you will receive a password reset link.");
},
onError: error => {
toast.error(handleAuthError(error, "Failed to send password reset email. Please try again."));
},
});
};
// Reset password hook
export const useResetPassword = () => {
const navigate = useNavigate();
return useMutation({
mutationFn: async ({ data, token }: { data: ResetPasswordFormData; token: string }) => {
if (!token) {
throw new Error("Reset token is missing");
}
setState({ isLoading: true, error: null });
try {
toast.promise(
authClient.resetPassword({
token,
newPassword: data.password,
}),
{
loading: "Resetting your password...",
success: () => {
router.push("/signin");
return "Your password has been reset successfully. You can now sign in with your new password.";
},
error: error =>
handleAuthError(error, "Failed to reset password. The link may have expired or is invalid."),
}
);
return true;
} catch (error) {
const errorMessage = handleAuthError(
error,
"Failed to reset password. The link may have expired or is invalid."
);
setState(prev => ({ ...prev, error: errorMessage }));
throw error;
} finally {
setState(prev => ({ ...prev, isLoading: false }));
}
await authClient.resetPassword({
token,
newPassword: data.password,
});
},
[router]
);
return {
...state,
resetPassword,
};
onSuccess: () => {
navigate({ to: "/signin" });
toast.success("Your password has been reset successfully. You can now sign in with your new password.");
},
onError: error => {
toast.error(handleAuthError(error, "Failed to reset password. The link may have expired or is invalid."));
},
});
};

View File

@@ -1,17 +0,0 @@
"use client";
import { useSearchParams as useNextSearchParams } from "next/navigation";
import { useCallback } from "react";
export const useSearchParamsSafely = () => {
const searchParams = useNextSearchParams();
const getParam = useCallback(
(key: string) => {
return searchParams?.get(key);
},
[searchParams]
);
return { getParam, searchParams };
};

View File

@@ -18,7 +18,6 @@ export interface UploadFileDialogProps {
export interface AuthCardProps extends ComponentProps<"div"> {
title: string;
description: string;
navigationType: "signin" | "signup";
children: ReactNode;
}

View File

@@ -1,4 +1,5 @@
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { LoadingStatePage } from "./components/loading-state-page";
import { routeTree } from "./routeTree.gen";
import ReactDOM from "react-dom/client";
import { StrictMode } from "react";
@@ -10,14 +11,7 @@ const router = createRouter({
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
context: undefined!,
defaultPendingComponent: () => (
<div className="flex h-screen w-full items-center justify-center">
<div className="flex flex-col items-center gap-2">
<div className="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
<p className="text-muted-foreground text-sm">Loading...</p>
</div>
</div>
),
defaultPendingComponent: () => LoadingStatePage,
});
// Register the router instance for type safety

View File

@@ -9,7 +9,7 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from "./routes/__root"
import { Route as DebugRouteImport } from "./routes/debug"
import { Route as DevelopersRouteImport } from "./routes/developers"
import { Route as PublicRouteImport } from "./routes/_public"
import { Route as ProtectedRouteImport } from "./routes/_protected"
import { Route as IndexRouteImport } from "./routes/index"
@@ -26,9 +26,9 @@ import { Route as ProtectedDashboardIndexRouteImport } from "./routes/_protected
import { Route as ProtectedDashboardSettingsRouteImport } from "./routes/_protected/dashboard/settings"
import { Route as ProtectedDashboardProviderSlugAccountIdRouteImport } from "./routes/_protected/dashboard/$providerSlug.$accountId"
const DebugRoute = DebugRouteImport.update({
id: "/debug",
path: "/debug",
const DevelopersRoute = DevelopersRouteImport.update({
id: "/developers",
path: "/developers",
getParentRoute: () => rootRouteImport,
} as any)
const PublicRoute = PublicRouteImport.update({
@@ -109,7 +109,7 @@ const ProtectedDashboardProviderSlugAccountIdRoute =
export interface FileRoutesByFullPath {
"/": typeof IndexRoute
"/debug": typeof DebugRoute
"/developers": typeof DevelopersRoute
"/dashboard": typeof ProtectedDashboardRouteWithChildren
"/contributors": typeof PublicContributorsRoute
"/forgot-password": typeof PublicForgotPasswordRoute
@@ -125,7 +125,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
"/": typeof IndexRoute
"/debug": typeof DebugRoute
"/developers": typeof DevelopersRoute
"/contributors": typeof PublicContributorsRoute
"/forgot-password": typeof PublicForgotPasswordRoute
"/privacy": typeof PublicPrivacyRoute
@@ -143,7 +143,7 @@ export interface FileRoutesById {
"/": typeof IndexRoute
"/_protected": typeof ProtectedRouteWithChildren
"/_public": typeof PublicRouteWithChildren
"/debug": typeof DebugRoute
"/developers": typeof DevelopersRoute
"/_protected/dashboard": typeof ProtectedDashboardRouteWithChildren
"/_public/contributors": typeof PublicContributorsRoute
"/_public/forgot-password": typeof PublicForgotPasswordRoute
@@ -161,7 +161,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| "/"
| "/debug"
| "/developers"
| "/dashboard"
| "/contributors"
| "/forgot-password"
@@ -177,7 +177,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| "/"
| "/debug"
| "/developers"
| "/contributors"
| "/forgot-password"
| "/privacy"
@@ -194,7 +194,7 @@ export interface FileRouteTypes {
| "/"
| "/_protected"
| "/_public"
| "/debug"
| "/developers"
| "/_protected/dashboard"
| "/_public/contributors"
| "/_public/forgot-password"
@@ -213,16 +213,16 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ProtectedRoute: typeof ProtectedRouteWithChildren
PublicRoute: typeof PublicRouteWithChildren
DebugRoute: typeof DebugRoute
DevelopersRoute: typeof DevelopersRoute
}
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
"/debug": {
id: "/debug"
path: "/debug"
fullPath: "/debug"
preLoaderRoute: typeof DebugRouteImport
"/developers": {
id: "/developers"
path: "/developers"
fullPath: "/developers"
preLoaderRoute: typeof DevelopersRouteImport
parentRoute: typeof rootRouteImport
}
"/_public": {
@@ -390,7 +390,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ProtectedRoute: ProtectedRouteWithChildren,
PublicRoute: PublicRouteWithChildren,
DebugRoute: DebugRoute,
DevelopersRoute: DevelopersRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -5,6 +5,7 @@ import { ThemeProvider } from "@/components/providers/theme-provider";
import { AppProviders } from "@/components/providers/app-providers";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { geistSans, geistMono, manrope } from "@/utils/fonts";
import { TanStackDevtools } from "@tanstack/react-devtools";
import { Toaster } from "sonner";
import { Suspense } from "react";
@@ -22,19 +23,25 @@ function RootComponent() {
<div
className={`bg-background text-foreground relative min-h-screen ${geistSans.variable} ${geistMono.variable} ${manrope.variable}`}
>
<Suspense fallback={<div className="flex h-screen items-center justify-center">Loading...</div>}>
<Suspense>
<Outlet />
</Suspense>
<Toaster position="top-center" richColors theme="system" />
<Toaster position="bottom-right" richColors theme="system" />
</div>
<Suspense fallback={null}>
<TanStackRouterDevtools position="bottom-right" />
</Suspense>
</ThemeProvider>
</AppProviders>
<Suspense fallback={null}>
<ReactQueryDevtools initialIsOpen={false} />
</Suspense>
<TanStackDevtools
plugins={[
{
name: "TanStack Router",
render: <TanStackRouterDevtools />,
},
{
name: "React Query",
render: <ReactQueryDevtools />,
},
]}
/>
</ReactQueryProvider>
);
}

View File

@@ -1,5 +1,6 @@
import { DefaultAccountProvider } from "@/components/providers/default-account-provider";
import { UserInfoProvider } from "@/components/providers/user-info-provider";
import { AccountProvider } from "@/components/providers/account-provider";
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/_protected/dashboard")({
@@ -8,10 +9,12 @@ export const Route = createFileRoute("/_protected/dashboard")({
function DashboardLayout() {
return (
<UserInfoProvider>
<DefaultAccountProvider>
<Outlet />
</DefaultAccountProvider>
</UserInfoProvider>
<AccountProvider>
<UserInfoProvider>
<DefaultAccountProvider>
<Outlet />
</DefaultAccountProvider>
</UserInfoProvider>
</AccountProvider>
);
}

View File

@@ -1,3 +1,4 @@
import { AccountProvider } from "@/components/providers/account-provider";
import DndKitProvider from "@/components/providers/dnd-kit-provider";
import { FileTable } from "@/components/dashboard/file-browser";
import { createFileRoute } from "@tanstack/react-router";
@@ -7,6 +8,8 @@ import { Suspense } from "react";
type DashboardSearch = {
folderId?: string;
type?: string;
id?: string;
};
export const Route = createFileRoute("/_protected/dashboard/$providerSlug/$accountId")({
@@ -14,6 +17,8 @@ export const Route = createFileRoute("/_protected/dashboard/$providerSlug/$accou
validateSearch: (search: Record<string, unknown>): DashboardSearch => {
return {
folderId: (search.folderId as string) || undefined,
type: (search.type as string) || undefined,
id: (search.id as string) || undefined,
};
},
});

View File

@@ -60,7 +60,7 @@ function SettingsPage() {
if (user?.email !== email) {
await authClient.changeEmail({
newEmail: email,
callbackURL: `${import.meta.env.VITE_FRONTEND_URL}/verify-email`,
callbackURL: `${env.VITE_FRONTEND_URL}/verify-email`,
});
isUpdated = true;
}

View File

@@ -4,6 +4,10 @@ import { Suspense } from "react";
export const Route = createFileRoute("/_public/reset-password")({
component: ResetPasswordPage,
validateSearch: (search: Record<string, string>) => ({
token: search.token as string,
error: search.error as string,
}),
});
function ResetPasswordContent() {

View File

@@ -7,7 +7,7 @@ export const Route = createFileRoute("/_public/signin")({
component: SigninPage,
validateSearch: (search: Record<string, unknown>) => {
return {
redirect: (search.redirect as string) || undefined,
redirectTo: (search.redirectTo as string) || undefined,
};
},
});

View File

@@ -4,6 +4,12 @@ import { Suspense } from "react";
export const Route = createFileRoute("/_public/verify-email")({
component: VerifyEmailPage,
validateSearch: (search: Record<string, unknown>) => {
return {
token: (search.token as string) || undefined,
error: (search.error as string) || undefined,
};
},
});
function VerifyEmailPage() {

View File

@@ -1,164 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { authClient } from "@nimbus/auth/auth-client";
import { useState, useEffect } from "react";
export const Route = createFileRoute("/debug")({
component: DebugPage,
});
function DebugPage() {
const [authState, setAuthState] = useState<any>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const checkAuth = async () => {
try {
const session = await authClient.getSession();
setAuthState(session);
} catch (error) {
setAuthError(error instanceof Error ? error.message : "Unknown error");
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
return (
<div className="bg-background min-h-screen p-8">
<div className="mx-auto max-w-4xl space-y-6">
<div className="border-border bg-card rounded-lg border p-6">
<h1 className="text-3xl font-bold">Debug Information</h1>
<p className="text-muted-foreground mt-2">TanStack Router + Vite Migration Debug Page</p>
</div>
{/* Router Status */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold"> Router Status</h2>
<p className="text-muted-foreground mt-2">If you can see this page, TanStack Router is working correctly!</p>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Current Path:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{window.location.pathname}</code>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Search:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{window.location.search || "(none)"}</code>
</div>
</div>
</div>
{/* Authentication Status */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold">🔐 Authentication Status</h2>
{loading ? (
<p className="text-muted-foreground mt-2">Checking authentication...</p>
) : authError ? (
<div className="mt-4">
<p className="text-red-600">Error: {authError}</p>
</div>
) : (
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Authenticated:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{authState?.user ? "Yes" : "No"}</code>
</div>
{authState?.user && (
<>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">User Email:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{authState.user.email}</code>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">User Name:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{authState.user.name || "(not set)"}</code>
</div>
</>
)}
</div>
)}
</div>
{/* Environment Check */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold">🌍 Environment</h2>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Mode:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.MODE}</code>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Dev:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.DEV ? "Yes" : "No"}</code>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Prod:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.PROD ? "Yes" : "No"}</code>
</div>
</div>
</div>
{/* Quick Links */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold">🔗 Quick Links</h2>
<div className="mt-4 grid grid-cols-2 gap-2">
<a href="/" className="text-primary hover:underline">
Home
</a>
<a href="/signin" className="text-primary hover:underline">
Sign In
</a>
<a href="/signup" className="text-primary hover:underline">
Sign Up
</a>
<a href="/dashboard" className="text-primary hover:underline">
Dashboard
</a>
<a href="/terms" className="text-primary hover:underline">
Terms
</a>
<a href="/privacy" className="text-primary hover:underline">
Privacy
</a>
<a href="/contributors" className="text-primary hover:underline">
Contributors
</a>
<a href="/nonexistent" className="text-primary hover:underline">
404 Test
</a>
</div>
</div>
{/* System Info */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold">💻 System Info</h2>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">User Agent:</span>
<code className="bg-muted rounded px-2 py-1 text-xs">{navigator.userAgent}</code>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm">Viewport:</span>
<code className="bg-muted rounded px-2 py-1 text-sm">
{window.innerWidth} x {window.innerHeight}
</code>
</div>
</div>
</div>
{/* Console Log */}
<div className="border-border bg-card rounded-lg border p-6">
<h2 className="text-xl font-semibold">📝 Instructions</h2>
<div className="text-muted-foreground mt-4 space-y-2 text-sm">
<p>1. Open your browser's developer console (F12)</p>
<p>2. Check the Console tab for any errors</p>
<p>3. Check the Network tab to see if all assets are loading</p>
<p>4. Try navigating to different routes using the links above</p>
<p>5. If you see a blank page on other routes, check the console for errors</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import tanstackRouter from "@tanstack/router-plugin/vite";
import { devtools } from "@tanstack/devtools-vite";
import tsconfigPaths from "vite-tsconfig-paths";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
@@ -6,6 +7,13 @@ import path from "path";
export default defineConfig({
plugins: [
devtools({
removeDevtoolsOnBuild: true,
logging: true,
enhancedLogs: {
enabled: true,
},
}),
tanstackRouter({
target: "react",
autoCodeSplitting: true,
@@ -34,4 +42,7 @@ export default defineConfig({
optimizeDeps: {
exclude: ["@nimbus/auth", "@nimbus/env", "@nimbus/server", "@nimbus/shared"],
},
define: {
"process.env": {},
},
});

View File

@@ -4,6 +4,8 @@
"": {
"name": "nimbus",
"dependencies": {
"@tanstack/devtools-vite": "^0.3.6",
"@tanstack/react-devtools": "^0.7.6",
"stripe": "^19.0.0",
},
"devDependencies": {
@@ -311,7 +313,7 @@
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@better-auth/stripe": ["@better-auth/stripe@1.3.24", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "better-auth": "1.3.24", "stripe": "^18" } }, "sha512-7Ib0w4FRbUSgWTVljLU/H7NKZzu8l1iJ0CdZgY24qKahpNy0yoTV/tQbaUv1eQ/bw4+X46g8EpsNZsxgdwDZJQ=="],
@@ -847,6 +849,14 @@
"@smithy/util-waiter": ["@smithy/util-waiter@4.0.7", "", { "dependencies": { "@smithy/abort-controller": "^4.0.5", "@smithy/types": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA=="],
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
@@ -883,12 +893,26 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.14", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "postcss": "^8.4.41", "tailwindcss": "4.1.14" } }, "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg=="],
"@tanstack/devtools": ["@tanstack/devtools@0.6.20", "", { "dependencies": { "@solid-primitives/keyboard": "^1.3.3", "@tanstack/devtools-event-bus": "0.3.2", "@tanstack/devtools-ui": "0.4.2", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-7Sw6bWvwKsHDNLg+8v7xOXhE5tzwx6/KgLWSSP55pJ86wpSXYdIm89vvXm4ED1lgKfEU5l3f4Y6QVagU4rgRiQ=="],
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.2", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-j4XPrLjaZ8GaUe9Lt0QOyfrv0Q2jy/9Og5nLQM5/cFcOLxuBp5mXR/fFrA9/9oVCIld/CxetMEac8CwayXVTHQ=="],
"@tanstack/devtools-event-bus": ["@tanstack/devtools-event-bus@0.3.2", "", { "dependencies": { "ws": "^8.18.3" } }, "sha512-yJT2As/drc+Epu0nsqCsJaKaLcaNGufiNxSlp/+/oeTD0jsBxF9/PJBfh66XVpYXkKr97b8689mSu7QMef0Rrw=="],
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.3", "", {}, "sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg=="],
"@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.4.2", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-xvALRLeD+TYjaLx9f9OrRBBZITAYPIk7RH8LRiESUQHw7lZO/sBU1ggrcSePh7TwKWXl9zLmtUi+7xVIS+j/dQ=="],
"@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.3.6", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.2", "@tanstack/devtools-event-bus": "0.3.2", "chalk": "^5.6.2", "launch-editor": "^2.11.1" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-OYzyRVxHvrEMJLYIicD5HekQTrQeEqVGpEEHnQuAXmZydRh4qXebUBV+XYDaz+AA6woRuAzvYXALCwBq0kNMfw=="],
"@tanstack/history": ["@tanstack/history@1.132.31", "", {}, "sha512-UCHM2uS0t/uSszqPEo+SBSSoQVeQ+LlOWAVBl5SA7+AedeAbKafIPjFn8huZCXNLAYb0WKV2+wETr7lDK9uz7g=="],
"@tanstack/query-core": ["@tanstack/query-core@5.85.5", "", {}, "sha512-KO0WTob4JEApv69iYp1eGvfMSUkgw//IpMnq+//cORBzXf0smyRwPLrUvEe5qtAEGjwZTXrjxg+oJNP/C00t6w=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.84.0", "", {}, "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ=="],
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.7.6", "", { "dependencies": { "@tanstack/devtools": "0.6.20" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-fP0jY7yed0HVIEhs+rjn8wZqABD/6TUiq6SV8jlyYP8NBK2Jfq3ce+IRw5w+N7KBzEokveLQFktxoLNpt3ZOkA=="],
"@tanstack/react-query": ["@tanstack/react-query@5.85.5", "", { "dependencies": { "@tanstack/query-core": "5.85.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/X4EFNcnPiSs8wM2v+b6DqS5mmGeuJQvxBglmDxl6ZQb5V26ouD2SJYAcC3VjbNwqhY2zjxVD15rDA5nGbMn3A=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.85.5", "", { "dependencies": { "@tanstack/query-devtools": "5.84.0" }, "peerDependencies": { "@tanstack/react-query": "^5.85.5", "react": "^18 || ^19" } }, "sha512-6Ol6Q+LxrCZlQR4NoI5181r+ptTwnlPG2t7H9Sp3klxTBhYGunONqcgBn2YKRPsaKiYM8pItpKMdMXMEINntMQ=="],
@@ -1453,6 +1477,8 @@
"kysely": ["kysely@0.28.5", "", {}, "sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA=="],
"launch-editor": ["launch-editor@2.11.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg=="],
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -1775,6 +1801,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
@@ -1949,6 +1977,8 @@
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
@@ -1967,36 +1997,14 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/helpers/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/parser/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/template/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@babel/traverse/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
"@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
@@ -2049,6 +2057,8 @@
"@tailwindcss/postcss/tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="],
"@tanstack/devtools-vite/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"@tanstack/react-router-devtools/vite": ["vite@7.1.9", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
@@ -2059,6 +2069,8 @@
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tanstack/router-plugin/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tanstack/router-utils/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -2067,11 +2079,7 @@
"@types/babel__core/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@types/babel__generator/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@types/babel__template/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@types/babel__traverse/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@types/babel__core/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@types/node-fetch/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
@@ -2079,8 +2087,6 @@
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"babel-dead-code-elimination/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"box-node-sdk/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],

View File

@@ -48,6 +48,8 @@
"packages/*"
],
"dependencies": {
"@tanstack/devtools-vite": "^0.3.6",
"@tanstack/react-devtools": "^0.7.6",
"stripe": "^19.0.0"
}
}

View File

@@ -2,9 +2,9 @@ import { type Account, type AuthContext, betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import schema, { user as userTable } from "@nimbus/db/schema";
import { cacheClient, type CacheClient } from "@nimbus/cache";
import { stripe } from "@better-auth/stripe";
// import { genericOAuth } from "better-auth/plugins";
import { sendMail } from "./utils/send-mail";
import { stripe } from "@better-auth/stripe";
import { env } from "@nimbus/env/server";
import { db, type DB } from "@nimbus/db";
import { eq } from "drizzle-orm";
@@ -52,7 +52,7 @@ export const auth = betterAuth({
minPasswordLength: 8,
maxPasswordLength: 100,
resetPasswordTokenExpiresIn: 600, // 10 minutes
requireEmailVerification: true,
// requireEmailVerification: true,
sendResetPassword: async ({ user, token: _token, url }) => {
// const frontendResetUrl = `${env.FRONTEND_URL}/reset-password?token=${token}`;
await sendMail(emailContext, {
@@ -63,20 +63,20 @@ export const auth = betterAuth({
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
// const urlParts = url.split(`${env.BACKEND_URL}/api/auth`);
// const emailUrl = `${env.FRONTEND_URL}${urlParts[1]}`;
await sendMail(emailContext, {
to: user.email,
subject: "Verify your Nimbus email address",
text: `Click the link to verify your email address: ${url}`,
});
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 3600, // 1 hour
},
// emailVerification: {
// sendVerificationEmail: async ({ user, url }) => {
// // const urlParts = url.split(`${env.BACKEND_URL}/api/auth`);
// // const emailUrl = `${env.FRONTEND_URL}${urlParts[1]}`;
// await sendMail(emailContext, {
// to: user.email,
// subject: "Verify your Nimbus email address",
// text: `Click the link to verify your email address: ${url}`,
// });
// },
// sendOnSignUp: true,
// autoSignInAfterVerification: true,
// expiresIn: 3600, // 1 hour
// },
socialProviders: {
google: {
@@ -118,7 +118,7 @@ export const auth = betterAuth({
plugins: [
stripe({
stripeClient,
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET!,
stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,
createCustomerOnSignUp: true,
}),
// genericOAuth({

View File

@@ -1,36 +0,0 @@
# @nimbus/shared
## 0.0.4
### Patch Changes
- @nimbus/db@0.0.3
## 0.0.3
### Patch Changes
- 03bb85d: chore: updated everything to latest except zod and Next.js (zod, tying errors, opennextjs-cloudflare only
supports 15.3) CMD: find . -type f -name "package.json" -not -path "_/node_modules/_" -not -path "*/.*next/\*" -exec
sh -c 'echo "\nUpdating $1/..." && (cd "$(dirname "$1")" && ncu -u)' \_ {} \; chore: revert zod upgrade as it breaks
better-auth typing chore: zod broke better-auth typing again... chore: revert back to base sql chore: auth clean
chore: FINALLY fixed schema.ts chore: reset migrations to current database state. removed the rateLimit table since we
use cache now
- Updated dependencies [de391ed]
- Updated dependencies [7ffde8d]
- Updated dependencies [03bb85d]
- @nimbus/db@0.0.2
## 0.0.2
### Patch Changes
- 1c1f4a3: Added legal pages. Added legal constants. Grouped all constants in shared/constants/\*.
## 0.0.1
### Patch Changes
- 7e2271f: init changeset
- Updated dependencies [7e2271f]
- @nimbus/db@0.0.1

View File

@@ -1,9 +1,7 @@
import z from "zod";
// const providers = ["google", "microsoft", "dropbox", "box", "nimbus", "apple", "github"] as const;
// const providerSlugs = ["g", "m", "d", "b", "n", "a", "gh"] as const;
const providers = ["google", "microsoft", "s3", "box", "dropbox"] as const;
const providerSlugs = ["g", "m", "s3", "b", "d"] as const;
const providers = ["credential", "google", "microsoft", "s3", "box", "dropbox"] as const;
const providerSlugs = ["c", "g", "m", "s3", "b", "d"] as const;
// Define social providers first
export const driveProviderSchema = z.enum(providers);

View File

@@ -4,7 +4,7 @@
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
"outputs": ["dist/**", "apps/**/dist/**", "packages/**/dist/**", "!.cache/**"]
},
"dev": {
"persistent": true,