From 6574c0a8b89e4b1bfb61c1c428bca4ae57dd5606 Mon Sep 17 00:00:00 2001 From: Bhargav Ponnapalli Date: Sat, 13 Jul 2024 01:10:31 +0530 Subject: [PATCH] styling improvements to onboarding flow --- .../settings/EditOrganizationForm.tsx | 2 +- .../onboarding/OnboardingFlow.tsx | 469 +++--------------- .../onboarding/OrganizationCreation.tsx | 87 ++++ .../onboarding/ProfileUpdate.tsx | 125 +++++ .../onboarding/TermsAcceptance.tsx | 70 +++ .../(authenticated-pages)/onboarding/page.tsx | 41 +- src/components/CreateOrganizationDialog.tsx | 2 +- src/components/CreateOrganizationForm.tsx | 6 +- src/styles/globals.css | 94 ++-- src/utils/zod-schemas/organization.ts | 8 + 10 files changed, 449 insertions(+), 455 deletions(-) create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OrganizationCreation.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/onboarding/ProfileUpdate.tsx create mode 100644 src/app/(dynamic-pages)/(authenticated-pages)/onboarding/TermsAcceptance.tsx create mode 100644 src/utils/zod-schemas/organization.ts diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/[organizationSlug]/(specific-organization-pages)/settings/EditOrganizationForm.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/[organizationSlug]/(specific-organization-pages)/settings/EditOrganizationForm.tsx index b17e3c87..6a9d14cd 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/[organizationSlug]/(specific-organization-pages)/settings/EditOrganizationForm.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/[organizationSlug]/(specific-organization-pages)/settings/EditOrganizationForm.tsx @@ -1,11 +1,11 @@ "use client"; -import { createOrganizationSchema } from "@/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow"; import { Button } from "@/components/Button"; import { T } from "@/components/ui/Typography"; import { Input } from "@/components/ui/input"; import { updateOrganizationInfo } from "@/data/user/organizations"; import { useSAToastMutation } from "@/hooks/useSAToastMutation"; import { generateSlug } from "@/lib/utils"; +import { createOrganizationSchema } from "@/utils/zod-schemas/organization"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { useForm, type SubmitHandler } from "react-hook-form"; diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx index 66382aaf..705e368f 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow.tsx @@ -1,358 +1,110 @@ "use client"; -import { T } from "@/components/ui/Typography"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; -import { createOrganization } from "@/data/user/organizations"; -import { - acceptTermsOfService, - updateUserProfileNameAndAvatar, - uploadPublicUserAvatar, -} from "@/data/user/user"; -import { useSAToastMutation } from "@/hooks/useSAToastMutation"; -import { generateSlug } from "@/lib/utils"; -import type { Table } from "@/types"; -import { getUserAvatarUrl } from "@/utils/helpers"; -import type { AuthUserMetadata } from "@/utils/zod-schemas/authUserMetadata"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { motion } from "framer-motion"; -import { UserPlus as AddUserIcon } from "lucide-react"; -import dynamic from "next/dynamic"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -const TermsDetailDialog = dynamic( - () => import("./TermsDetailDialog").then((mod) => mod.TermsDetailDialog), - { - ssr: false, - loading: () => , - }, -); -const MotionImage = motion(Image); +import { AnimatePresence, motion } from "framer-motion"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; -type TermsAcceptanceProps = { - onSuccess: () => void; -}; +import { Card } from "@/components/ui/card"; -function TermsAcceptance({ onSuccess }: TermsAcceptanceProps) { - const { mutate: acceptTerms, isLoading } = useSAToastMutation( - async () => { - return acceptTermsOfService(true); - }, - { - successMessage: "Terms accepted!", - errorMessage: "Failed to accept terms", - onSuccess, - }, - ); +import { OrganizationCreation } from "./OrganizationCreation"; +import { ProfileUpdate } from "./ProfileUpdate"; +import { TermsAcceptance } from "./TermsAcceptance"; - return ( - - - πŸŽ‰ Welcome Aboard! - - Before diving into Nextbase Ultimate starter kit, please take a moment - to go through our updated Terms of Service. - - - -
- - These terms and conditions govern the use of Nextbase starter kit’s - products and services. They're designed to ensure a smooth and - secure experience for you. - +import type { Table } from "@/types"; +import type { AuthUserMetadata } from "@/utils/zod-schemas/authUserMetadata"; - - Last updated : 24th April 2024 - -
-
- - - -
- ); -} +type FLOW_STATE = "TERMS" | "PROFILE" | "ORGANIZATION" | "COMPLETE"; -type ProfileUpdateProps = { +type UserOnboardingFlowProps = { userProfile: Table<"user_profiles">; - onSuccess: () => void; + onboardingStatus: AuthUserMetadata; userEmail: string | undefined; }; -export function ProfileUpdate({ +const MotionCard = motion(Card); + +export function UserOnboardingFlow({ userProfile, - onSuccess, + onboardingStatus, userEmail, -}: ProfileUpdateProps) { - const [fullName, setFullName] = useState(userProfile.full_name ?? ""); - const [avatarUrl, setAvatarUrl] = useState( - userProfile.avatar_url ?? undefined, +}: UserOnboardingFlowProps) { + const flowStates = useMemo(() => getAllFlowStates(onboardingStatus), [onboardingStatus]); + const [currentStep, setCurrentStep] = useState( + getInitialFlowState(flowStates, onboardingStatus) ); - const [isUploading, setIsUploading] = useState(false); - const fileInputRef = useRef(null); - const [hasImageLoaded, setHasImageLoaded] = useState(false); - - const avatarUrlWithFallback = getUserAvatarUrl({ - profileAvatarUrl: avatarUrl ?? userProfile.avatar_url, - email: userEmail, - }); - - const { mutate: updateProfile, isLoading: isUpdatingProfile } = - useSAToastMutation( - async () => { - return await updateUserProfileNameAndAvatar( - { fullName, avatarUrl }, - { - isOnboardingFlow: true, - }, - ); - }, - { - successMessage: "Profile updated!", - errorMessage: "Failed to update profile", - onSuccess, - }, - ); - - const { mutate: uploadAvatar } = useSAToastMutation( - async (file: File) => { - const formData = new FormData(); - formData.append("file", file); - - const newAvatarUrl = await uploadPublicUserAvatar(formData, file.name, { - upsert: true, - }); + const { replace } = useRouter(); - return newAvatarUrl; - }, - { - onMutate: () => { - setIsUploading(true); - }, - successMessage: "Avatar uploaded!", - errorMessage: "Error uploading avatar", - onSuccess: (response) => { - console.log(response); - if (response.status === 'success') { - setIsUploading(false); - setAvatarUrl(response.data); - } - } - }, - ); + const nextStep = useCallback(() => { + const currentIndex = flowStates.indexOf(currentStep); + if (currentIndex < flowStates.length - 1) { + setCurrentStep(flowStates[currentIndex + 1]); + } + }, [currentStep, flowStates]); - const handleFileChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - uploadAvatar(file); + useEffect(() => { + if (currentStep === "COMPLETE") { + replace("/dashboard"); } + }, [currentStep, replace]); + + const cardVariants = { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -50 }, }; return ( - -
{ - e.preventDefault(); - updateProfile(); - }} - data-testid="create-new-profile" + + - -
-
- -
-
- Create new profile - Please fill in your details. -
-
-
- -
-
- -
-
- { - setHasImageLoaded(true); - }} - onLoadStart={() => { - setHasImageLoaded(false); - }} - placeholder="blur" - blurDataURL="data:image/png;base64,iVBORw0KGg0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" - loading="eager" - width={24} - height={24} - className="h-12 w-12 rounded-full" - src={avatarUrlWithFallback} - alt="avatarUrl" - /> - - -
-
-
-
- - setFullName(e.target.value)} - placeholder="Full Name" - type="text" - required - /> -
-
-
- - - - -
+ {currentStep === "TERMS" && } + {currentStep === "PROFILE" && ( + + )} + {currentStep === "ORGANIZATION" && ( + + )} + + ); } -type OrganizationCreationProps = { - onSuccess: () => void; -}; - -export const createOrganizationSchema = z.object({ - organizationTitle: z.string().min(1), - organizationSlug: z.string().min(1), -}); - -export type CreateOrganizationSchema = z.infer; - -export function OrganizationCreation({ onSuccess }: OrganizationCreationProps) { - const { mutate: createOrg, isLoading: isCreatingOrg = false } = useSAToastMutation( - async ({ organizationTitle, organizationSlug }: { organizationTitle: string, organizationSlug: string }) => { - const orgId = await createOrganization(organizationTitle, organizationSlug, { - isOnboardingFlow: true, - }); - return orgId; - }, - { - successMessage: "Organization created!", - errorMessage: "Failed to create organization", - onSuccess, - }, - ); - - const onSubmit = (data: CreateOrganizationSchema) => { - createOrg({ organizationTitle: data.organizationTitle, organizationSlug: data.organizationSlug }); - }; - - const { register, formState, handleSubmit, setValue } = - useForm({ - resolver: zodResolver(createOrganizationSchema), - }); +function getAllFlowStates(onboardingStatus: AuthUserMetadata): FLOW_STATE[] { + const { + onboardingHasAcceptedTerms, + onboardingHasCompletedProfile, + onboardingHasCreatedOrganization, + } = onboardingStatus; + const flowStates: FLOW_STATE[] = []; - console.log(formState.errors, formState.isValid); + if (!onboardingHasAcceptedTerms) { + flowStates.push("TERMS"); + } + if (!onboardingHasCompletedProfile) { + flowStates.push("PROFILE"); + } + if (!onboardingHasCreatedOrganization) { + flowStates.push("ORGANIZATION"); + } + flowStates.push("COMPLETE"); - return ( - -
- - Create Organization - - Please provide a name for your first organization. - - - -
- - { - setValue("organizationSlug", generateSlug(event.target.value), { shouldValidate: true }); - setValue("organizationTitle", event.target.value, { shouldValidate: true }); - }} - disabled={isCreatingOrg} - /> -
-
- - -
-
- - - -
-
- ); + return flowStates; } -type FLOW_STATE = "TERMS" | "PROFILE" | "ORGANIZATION" | "COMPLETE"; - -type UserOnboardingFlowProps = { - userProfile: Table<"user_profiles">; - onboardingStatus: AuthUserMetadata; - userEmail: string | undefined; -}; - function getInitialFlowState( flowStates: FLOW_STATE[], - onboardingStatus: AuthUserMetadata, + onboardingStatus: AuthUserMetadata ): FLOW_STATE { const { onboardingHasAcceptedTerms, @@ -377,70 +129,3 @@ function getInitialFlowState( return "COMPLETE"; } - -function getAllFlowStates(onboardingStatus: AuthUserMetadata): FLOW_STATE[] { - const { - onboardingHasAcceptedTerms, - onboardingHasCompletedProfile, - onboardingHasCreatedOrganization, - } = onboardingStatus; - const flowStates: FLOW_STATE[] = []; - if (!onboardingHasAcceptedTerms) { - flowStates.push("TERMS"); - } - if (!onboardingHasCompletedProfile) { - flowStates.push("PROFILE"); - } - if (!onboardingHasCreatedOrganization) { - flowStates.push("ORGANIZATION"); - } - flowStates.push("COMPLETE"); - return flowStates; -} - -export function UserOnboardingFlow({ - userProfile, - onboardingStatus, - userEmail, -}: UserOnboardingFlowProps) { - const flowStates = useMemo( - () => getAllFlowStates(onboardingStatus), - [onboardingStatus], - ); - const initialStep = useMemo( - () => getInitialFlowState(flowStates, onboardingStatus), - [flowStates, onboardingStatus], - ); - const [currentStep, setCurrentStep] = useState(initialStep); - const nextStep = useCallback(() => { - const currentIndex = flowStates.indexOf(currentStep); - if (currentIndex < flowStates.length - 1) { - setCurrentStep(flowStates[currentIndex + 1]); - } - }, [currentStep, flowStates]); - - const { replace } = useRouter(); - - useEffect(() => { - if (currentStep === "COMPLETE") { - // Redirect to dashboard - replace("/dashboard"); - } - }, [currentStep, replace]); - - return ( - <> - {currentStep === "TERMS" && } - {currentStep === "PROFILE" && ( - - )} - {currentStep === "ORGANIZATION" && ( - - )} - - ); -} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OrganizationCreation.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OrganizationCreation.tsx new file mode 100644 index 00000000..99011dd0 --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/OrganizationCreation.tsx @@ -0,0 +1,87 @@ +import { Button } from "@/components/ui/button"; +import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/components/ui/use-toast"; +import { createOrganization } from "@/data/user/organizations"; +import { generateSlug } from "@/lib/utils"; +import { CreateOrganizationSchema, createOrganizationSchema } from "@/utils/zod-schemas/organization"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; + + + + +type OrganizationCreationProps = { + onSuccess: () => void; +}; + +export function OrganizationCreation({ onSuccess }: OrganizationCreationProps) { + const { toast } = useToast(); + const { register, handleSubmit, setValue, formState: { errors, isValid } } = useForm({ + resolver: zodResolver(createOrganizationSchema), + }); + + const createOrgMutation = useMutation({ + mutationFn: ({ organizationTitle, organizationSlug }: CreateOrganizationSchema) => + createOrganization(organizationTitle, organizationSlug, { isOnboardingFlow: true }), + onSuccess: () => { + toast({ title: "Organization created!", description: "Your new organization is ready." }); + onSuccess(); + }, + onError: () => { + toast({ title: "Failed to create organization", description: "Please try again.", variant: "destructive" }); + }, + }); + + const onSubmit = (data: CreateOrganizationSchema) => { + createOrgMutation.mutate(data); + }; + + return ( +
+ + Create Your Organization + Set up your first organization. + + +
+ + { + setValue("organizationSlug", generateSlug(e.target.value), { shouldValidate: true }); + setValue("organizationTitle", e.target.value, { shouldValidate: true }); + }} + /> + {errors.organizationTitle && ( +

{errors.organizationTitle.message}

+ )} +
+
+ + + {errors.organizationSlug && ( +

{errors.organizationSlug.message}

+ )} +
+
+ + + +
+ ); +} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/ProfileUpdate.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/ProfileUpdate.tsx new file mode 100644 index 00000000..c6a1d832 --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/ProfileUpdate.tsx @@ -0,0 +1,125 @@ +import { Button } from "@/components/ui/button"; +import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/components/ui/use-toast"; +import { updateUserProfileNameAndAvatar, uploadPublicUserAvatar } from "@/data/user/user"; +import type { Table } from "@/types"; +import { getUserAvatarUrl } from "@/utils/helpers"; +import { useMutation } from "@tanstack/react-query"; +import Image from "next/image"; +import { useState } from "react"; + +type ProfileUpdateProps = { + userProfile: Table<"user_profiles">; + onSuccess: () => void; + userEmail: string | undefined; +}; + +export function ProfileUpdate({ + userProfile, + onSuccess, + userEmail, +}: ProfileUpdateProps) { + const [fullName, setFullName] = useState(userProfile.full_name ?? ""); + const [avatarUrl, setAvatarUrl] = useState(userProfile.avatar_url ?? undefined); + const { toast } = useToast(); + + const avatarUrlWithFallback = getUserAvatarUrl({ + profileAvatarUrl: avatarUrl ?? userProfile.avatar_url, + email: userEmail, + }); + + const updateProfileMutation = useMutation({ + mutationFn: () => updateUserProfileNameAndAvatar({ fullName, avatarUrl }, { isOnboardingFlow: true }), + onSuccess: () => { + toast({ title: "Profile updated!", description: "Your profile has been successfully updated." }); + onSuccess(); + }, + onError: () => { + toast({ title: "Failed to update profile", description: "Please try again.", variant: "destructive" }); + }, + }); + + const uploadAvatarMutation = useMutation({ + mutationFn: (file: File) => { + const formData = new FormData(); + formData.append("file", file); + return uploadPublicUserAvatar(formData, file.name, { upsert: true }); + }, + onSuccess: (response) => { + if (response.status === 'success') { + setAvatarUrl(response.data); + toast({ title: "Avatar uploaded!", description: "Your new avatar has been set." }); + } + }, + onError: () => { + toast({ title: "Error uploading avatar", description: "Please try again.", variant: "destructive" }); + }, + }); + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + uploadAvatarMutation.mutate(file); + } + }; + + return ( +
{ + e.preventDefault(); + updateProfileMutation.mutate(); + }}> + + Create Your Profile + Let's set up your personal details. + + +
+ +
+ User avatar + +
+
+
+ + setFullName(e.target.value)} + placeholder="Your full name" + disabled={updateProfileMutation.isLoading} + /> +
+
+ + + +
+ ); +} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/TermsAcceptance.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/TermsAcceptance.tsx new file mode 100644 index 00000000..a7c5a6ab --- /dev/null +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/TermsAcceptance.tsx @@ -0,0 +1,70 @@ +import { Button } from "@/components/ui/button"; +import { CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { useToast } from "@/components/ui/use-toast"; +import { acceptTermsOfService } from "@/data/user/user"; +import { useMutation } from "@tanstack/react-query"; + +type TermsAcceptanceProps = { + onSuccess: () => void; +}; + +export function TermsAcceptance({ onSuccess }: TermsAcceptanceProps) { + const { toast } = useToast(); + const acceptTermsMutation = useMutation({ + mutationFn: () => acceptTermsOfService(true), + onSuccess: () => { + toast({ title: "Terms accepted!", description: "Welcome aboard!" }); + onSuccess(); + }, + onError: () => { + toast({ title: "Failed to accept terms", description: "Please try again.", variant: "destructive" }); + }, + }); + + return ( + <> + + πŸŽ‰ Welcome Aboard! + + Before diving in, please review our updated Terms of Service. + + + +

+ These terms govern the use of our products and services, ensuring a smooth and secure experience for you. +

+

+ Last updated: +

+
+ + + + + + + + Terms and Conditions + + Please read our terms and conditions carefully. + + +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit...

+

Ut tempor quam eget lectus consequat, id egestas nisi semper...

+
+ + + +
+
+
+ + ); +} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx index 8d59a276..e89e21e5 100644 --- a/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx +++ b/src/app/(dynamic-pages)/(authenticated-pages)/onboarding/page.tsx @@ -1,12 +1,11 @@ - +import { Skeleton } from "@/components/ui/skeleton"; import { fetchSlimOrganizations, getDefaultOrganization, setDefaultOrganization } from "@/data/user/organizations"; import { getUserProfile } from "@/data/user/user"; import { serverGetLoggedInUser } from "@/utils/server/serverGetLoggedInUser"; import { authUserMetadataSchema } from "@/utils/zod-schemas/authUserMetadata"; +import { Suspense } from 'react'; import { UserOnboardingFlow } from "./OnboardingFlow"; - - async function getDefaultOrganizationOrSet(): Promise { const [slimOrganizations, defaultOrganizationId] = await Promise.all([ fetchSlimOrganizations(), @@ -30,7 +29,7 @@ async function getDefaultOrganizationOrSet(): Promise { return firstOrganization.id; } -const getOnboardingConditions = async (userId: string) => { +async function getOnboardingConditions(userId: string) { const [userProfile, defaultOrganizationId] = await Promise.all([ getUserProfile(userId), getDefaultOrganizationOrSet(), @@ -40,19 +39,33 @@ const getOnboardingConditions = async (userId: string) => { userProfile, defaultOrganizationId, }; -}; +} -export default async function Onboarding() { - const user = await serverGetLoggedInUser(); - const { - userProfile, - } = await getOnboardingConditions(user.id) - const onboardingStatus = authUserMetadataSchema.parse(user.user_metadata) - return
+async function OnboardingFlowWrapper({ userId, userEmail }: { userId: string; userEmail: string | undefined }) { + const [onboardingConditions, user] = await Promise.all([ + getOnboardingConditions(userId), + serverGetLoggedInUser(), + ]); + const { userProfile } = onboardingConditions; + const onboardingStatus = authUserMetadataSchema.parse(user.user_metadata); + + return ( -
+ ); +} + +export default async function OnboardingPage() { + const user = await serverGetLoggedInUser(); + + return ( +
+ }> + + +
+ ); } diff --git a/src/components/CreateOrganizationDialog.tsx b/src/components/CreateOrganizationDialog.tsx index dd9c1638..0d281a13 100644 --- a/src/components/CreateOrganizationDialog.tsx +++ b/src/components/CreateOrganizationDialog.tsx @@ -1,5 +1,4 @@ "use client"; -import { createOrganizationSchema, type CreateOrganizationSchema } from "@/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -15,6 +14,7 @@ import { Label } from "@/components/ui/label"; import { createOrganization } from "@/data/user/organizations"; import { useSAToastMutation } from "@/hooks/useSAToastMutation"; import { generateSlug } from "@/lib/utils"; +import { CreateOrganizationSchema, createOrganizationSchema } from "@/utils/zod-schemas/organization"; import { zodResolver } from "@hookform/resolvers/zod"; import { Network, Plus } from "lucide-react"; import { useRouter } from "next/navigation"; diff --git a/src/components/CreateOrganizationForm.tsx b/src/components/CreateOrganizationForm.tsx index c90b6532..1247bdd4 100644 --- a/src/components/CreateOrganizationForm.tsx +++ b/src/components/CreateOrganizationForm.tsx @@ -1,8 +1,5 @@ "use client"; -import { - createOrganizationSchema, - type CreateOrganizationSchema, -} from "@/app/(dynamic-pages)/(authenticated-pages)/onboarding/OnboardingFlow"; + import { Button } from "@/components/ui/button"; import { Dialog, @@ -17,6 +14,7 @@ import { Label } from "@/components/ui/label"; import { createOrganization } from "@/data/user/organizations"; import { useSAToastMutation } from "@/hooks/useSAToastMutation"; import { generateSlug } from "@/lib/utils"; +import { CreateOrganizationSchema, createOrganizationSchema } from "@/utils/zod-schemas/organization"; import { zodResolver } from "@hookform/resolvers/zod"; import { Network } from "lucide-react"; import { redirect } from "next/navigation"; diff --git a/src/styles/globals.css b/src/styles/globals.css index 42dc5ebb..87acbb46 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -5,57 +5,65 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 224 71.4% 4.1%; + --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; - --card-foreground: 224 71.4% 4.1%; + --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; - --popover-foreground: 224 71.4% 4.1%; - --primary: 262.1 83.3% 57.8%; - --primary-foreground: 210 20% 98%; - --secondary: 220 14.3% 95.9%; - --secondary-foreground: 220.9 39.3% 11%; - --muted: 220 14.3% 95.9%; - --muted-foreground: 220 8.9% 46.1%; - --accent: 220 14.3% 95.9%; - --accent-foreground: 220.9 39.3% 11%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 20% 98%; - --border: 220 13% 91%; - --input: 220 13% 91%; - --ring: 262.1 83.3% 57.8%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; --radius: 0.5rem; - --chart-1: 225.28 76.81% 13.53%; - --chart-2: 225.28 76.81% 13.53%; - --chart-3: 225 77% 35%; - --chart-4: 225 77% 45%; - --chart-5: 225 77% 55%; } .dark { - --background: 224 71.4% 4.1%; - --foreground: 210 20% 98%; - --card: 224 71.4% 4.1%; - --card-foreground: 210 20% 98%; - --popover: 224 71.4% 4.1%; - --popover-foreground: 210 20% 98%; - --primary: 263.4 70% 50.4%; - --primary-foreground: 210 20% 98%; - --secondary: 215 27.9% 16.9%; - --secondary-foreground: 210 20% 98%; - --muted: 215 27.9% 16.9%; - --muted-foreground: 217.9 10.6% 64.9%; - --accent: 215 27.9% 16.9%; - --accent-foreground: 210 20% 98%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 20% 98%; - --border: 215 27.9% 16.9%; - --input: 215 27.9% 16.9%; - --ring: 263.4 70% 50.4%; - --chart-1: 225.28 76.81% 13.53%; - --chart-2: 225.28 76.81% 13.53%; - --chart-3: 225 77% 35%; - --chart-4: 225 77% 45%; - --chart-5: 225 77% 55%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9; + } +} + +@layer base { + :root { + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/src/utils/zod-schemas/organization.ts b/src/utils/zod-schemas/organization.ts new file mode 100644 index 00000000..fad900b4 --- /dev/null +++ b/src/utils/zod-schemas/organization.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const createOrganizationSchema = z.object({ + organizationTitle: z.string().min(1), + organizationSlug: z.string().min(1), +}); + +export type CreateOrganizationSchema = z.infer;