mirror of
https://github.com/nimbusdotstorage/Nimbus
synced 2026-04-22 17:45:03 +02:00
MAKE IT WORK BABBBYYYY
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
226
.github/workflows/cd.yml
vendored
226
.github/workflows/cd.yml
vendored
@@ -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
|
||||
124
.github/workflows/ci.yml
vendored
124
.github/workflows/ci.yml
vendored
@@ -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
|
||||
@@ -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...
|
||||
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { PasswordInputProps } from "@/lib/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>>({
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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>
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { type ComponentProps } from "react";
|
||||
|
||||
import { Sidebar, SidebarContent, SidebarHeader } from "@/components/ui/sidebar";
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use client";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback } from "react";
|
||||
|
||||
type AuthContextType = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
|
||||
import { DownloadProgress } from "@/components/ui/download-progress";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { RouteGuard } from "./route-guard";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { RouteGuard } from "./route-guard";
|
||||
|
||||
interface PublicRouteProps {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Fallback, Image, Root } from "@radix-ui/react-avatar";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Root, Indicator } from "@radix-ui/react-checkbox";
|
||||
import type { ComponentProps } from "react";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Root,
|
||||
CollapsibleTrigger as CollapsibleTriggerPrimitive,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle, Loader2, XCircle } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
import * as React from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Root,
|
||||
Portal,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Root } from "@radix-ui/react-label";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Indicator, Root } from "@radix-ui/react-progress";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Root } from "@radix-ui/react-separator";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Root, List, Trigger, Content } from "@radix-ui/react-tabs";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { Provider, Root, Trigger, Content, Portal, Arrow } from "@radix-ui/react-tooltip";
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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."));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -18,7 +18,6 @@ export interface UploadFileDialogProps {
|
||||
|
||||
export interface AuthCardProps extends ComponentProps<"div"> {
|
||||
title: string;
|
||||
description: string;
|
||||
navigationType: "signin" | "signup";
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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": {},
|
||||
},
|
||||
});
|
||||
|
||||
66
bun.lock
66
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tanstack/devtools-vite": "^0.3.6",
|
||||
"@tanstack/react-devtools": "^0.7.6",
|
||||
"stripe": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
"outputs": ["dist/**", "apps/**/dist/**", "packages/**/dist/**", "!.cache/**"]
|
||||
},
|
||||
"dev": {
|
||||
"persistent": true,
|
||||
|
||||
Reference in New Issue
Block a user