• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

The right way to Construct a Multi-Tenant SaaS Utility with Subsequent.js (Frontend Integration) — SitePoint

Admin by Admin
April 10, 2025
Home Coding
Share on FacebookShare on Twitter


Within the first a part of this text collection, we carried out the backend with Appwrite, put in some dependencies, and arrange Allow to deal with authorization and role-based entry management. 

Now let’s have a look at how we are able to combine the frontend with the backend for a completely useful EdTech SaaS software.

Frontend Integration: Implementing Authorization in Subsequent.js

Now that you’ve got backend authorization in place utilizing Allow, combine it into your Subsequent.js frontend. The frontend ought to:

  • Fetch consumer permissions from the backend to manage what customers can see and do.
  • Guarantee API requests respect role-based entry management (RBAC).
  • Conceal UI parts for unauthorized customers (e.g., forestall college students from seeing “Create Task”).

1. Organising API calls with authorization

Since solely the backend enforces permissions, your frontend by no means decides entry instantly—as an alternative, it:

  1. Sends requests to the backend
  2. Waits for the backend’s authorization response
  3. Shows knowledge or UI parts accordingly

To get began, you’ll have to have Node.js put in in your laptop.

Then, observe these steps, observe the steps under:

npx create-next-app@newest frontend
cd frontend

2. Initialize shadcn

What you’ll observe after the creation of your Nextjs venture is that Tailwind CSS v4 is put in for you proper out of the field, which implies you don’t have to do the rest. As a result of we’re making use of a part library, we’re going to set up Shadcn UI. 

To try this we have to run the init command to create a parts.json file within the root of the folder:

After initialization, you can begin including parts to your venture:

npx shadcn@newest add button card dialog enter label desk choose tabs

If requested, when you ought to use drive due to the Nextjs 15 model compatibility with shadcn, hit enter to proceed.

3. Set up wanted packages

Set up the next packages:

npm i lucide-react zustand
npm i --save-dev axios

Now that we’ve got put in all we have to construct our software, we are able to begin creating our different parts and routes.

To keep up UI consistency all through the appliance, paste this code into your world.css file (paste it under your tailwindcss import):

@layer base {
  :root {
    --background: 75 29% 95%;           
    --foreground: 0 0% 9%;              

    --card: 0 0% 100%;                  
    --card-foreground: 0 0% 9%;         

    --popover: 0 0% 99%;                
    --popover-foreground: 0 0% 9%;      

    --primary: 0 0% 0%;                 
    --primary-foreground: 60 100% 100%; 

    --secondary: 75 31% 95%;            
    --secondary-foreground: 0 0% 9%;    

    --muted: 69 30% 95%;                
    --muted-foreground: 0 0% 45%;       

    --accent: 252 29% 97%;              
    --accent-foreground: 0 0% 9%;       

    --destructive: 0 84.2% 60.2%;       
    --destructive-foreground: 0 0% 98%; 

    --border: 189 0% 45%;               
    --input: 155 0% 45%;                
    --ring: 0 0% 0%;                    

    --radius: 0.5rem;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  physique {
    @apply bg-background text-foreground;
  }
}
physique {
  font-family: Arial, Helvetica, sans-serif;
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  physique {
    @apply bg-background text-foreground;
  }
}

4. Element information

Create the next part information and paste their corresponding code:

  • AddAssignmentDialog.tsx file:
"use consumer"

import kind React from "react"

import { useState } from "react"
import { Button } from "@/parts/ui/button"
import { Enter } from "@/parts/ui/enter"
import { Label } from "@/parts/ui/label"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/parts/ui/dialog"
import { Task } from "@/sorts"

interface AddAssignmentDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  onAddAssignment: (knowledge: Task) => void
  creatorEmail: string
}

export perform AddAssignmentDialog({ open, onOpenChange, onAddAssignment, creatorEmail }: AddAssignmentDialogProps) {
  const [title, setTitle] = useState("")
  const [subject, setSubject] = useState("")
  const [teacher, setTeacher] = useState("")
  const [className, setClassName] = useState("")
  const [dueDate, setDueDate] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    
    const newAssignment = { title, topic, instructor, className, dueDate, creatorEmail }
    onAddAssignment(newAssignment)
    console.log("New project:", { title, topic, class: className, dueDate, creatorEmail })
    onOpenChange(false)
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Add New Task</DialogTitle>
          <DialogDescription>
            Enter the small print of the new project right here. Click on save while you're accomplished.
          </DialogDescription>
        </DialogHeader>
        <type onSubmit={handleSubmit}>
          <div className="grid gap-4 py-4">
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="title" className="text-right">
                Title
              </Label>
              <Enter id="title" worth={title} onChange={(e) => setTitle(e.goal.worth)} className="col-span-3" />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="topic" className="text-right">
                Topic
              </Label>
              <Enter id="topic" worth={topic} onChange={(e) => setSubject(e.goal.worth)} className="col-span-3" />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="instructor" className="text-right">
                Instructor
              </Label>
              <Enter id="instructor" worth={instructor} onChange={(e) => setTeacher(e.goal.worth)} className="col-span-3" />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="class" className="text-right">
                Class
              </Label>
              <Enter
                id="class"
                worth={className}
                onChange={(e) => setClassName(e.goal.worth)}
                className="col-span-3"
              />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="dueDate" className="text-right">
                Due Date
              </Label>
              <Enter
                id="dueDate"
                kind="date"
                worth={dueDate}
                onChange={(e) => setDueDate(e.goal.worth)}
                className="col-span-3"
              />
            </div>
          </div>
          <DialogFooter>
            <Button kind="submit">Save modifications</Button>
          </DialogFooter>
        </type>
      </DialogContent>
    </Dialog>
  )
}

This file defines a React part, AddAssignmentDialog, which renders a dialog type for including new assignments. It manages type state utilizing useState and submits the project knowledge to a mum or dad part by way of the onAddAssignment prop. The dialog contains enter fields for title, topic, instructor, class, and due date, and closes upon submission.

  • AddStudentDialog.tsx file:
'use consumer'

import { useState } from 'react'
import { Button } from '@/parts/ui/button'
import { Enter } from '@/parts/ui/enter'
import { Label } from '@/parts/ui/label'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/parts/ui/dialog'
import {
  Choose,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/parts/ui/choose"
import { Pupil } from '@/sorts'

interface AddStudentDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  onAddStudent: (knowledge: Pupil) => void
  loading: boolean
  creatorEmail: string
}

export perform AddStudentDialog({ open, onOpenChange, onAddStudent, loading, creatorEmail }: AddStudentDialogProps) {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')
  const [className, setClassName] = useState('')
  const [gender, setGender] = useState('')
  const [age, setAge] = useState("")

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    
    onAddStudent({
      firstName,
      lastName,
      className,
      gender,
      age: Quantity(age),
      creatorEmail
    })
    console.log('New pupil:', { firstName, lastName, className, gender, age })
    onOpenChange(false)
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Add New Pupil</DialogTitle>
          <DialogDescription>
            Enter the small print of the new pupil right here. Click on save while you're accomplished.
          </DialogDescription>
        </DialogHeader>
        <type onSubmit={handleSubmit}>
          <div className="grid gap-4 py-4">
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="firstName" className="text-right">
                First Title
              </Label>
              <Enter
                id="firstName"
                worth={firstName}
                onChange={(e) => setFirstName(e.goal.worth)}
                className="col-span-3"
              />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="lastName" className="text-right">
                Final Title
              </Label>
              <Enter
                id="lastName"
                worth={lastName}
                onChange={(e) => setLastName(e.goal.worth)}
                className="col-span-3"
              />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="class" className="text-right">
                Class
              </Label>
              <Enter
                id="class"
                worth={className}
                onChange={(e) => setClassName(e.goal.worth)}
                className="col-span-3"
              />
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="gender" className="text-right">
                Gender
              </Label>
              <Choose onValueChange={setGender} worth={gender}>
                <SelectTrigger className="col-span-3">
                  <SelectValue placeholder="Choose gender" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem worth="boy">Boy</SelectItem>
                  <SelectItem worth="woman">Lady</SelectItem>
                </SelectContent>
              </Choose>
            </div>
            <div className="grid grid-cols-4 items-center gap-4">
              <Label htmlFor="age" className="text-right">
                age
              </Label>
              <Enter
                id="age"
                kind="quantity"
                step="0.1"
                worth={age}
                min={"4"}
                max={"99"}
                placeholder='enter a legitimate age'
                onChange={(e) => setAge(e.goal.worth)}
                className="col-span-3"
              />
            </div>
          </div>
          <DialogFooter>
            <Button disabled={loading} kind="submit">{loading ? "Saving..." : "Save Modifications"}</Button>
          </DialogFooter>
        </type>
      </DialogContent>
    </Dialog>
  )
}

This file defines a React part, AddStudentDialog, which renders a dialog type for including new college students. It manages type state utilizing useState and submits the scholar knowledge to a mum or dad part by way of the onAddStudent prop. The dialog contains enter fields for first identify, final identify, class, gender (with a dropdown), and age, and handles loading states throughout submission.

  • AssignmentsTable.tsx file:
import { Desk, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/parts/ui/desk"
import kind { AssignmentsTable } from "@/sorts"

export perform AssignmentsTables({ assignments }: { assignments: AssignmentsTable[] }) {
  console.log("Assignments", assignments)

  return (
    <Desk>
      <TableCaption>A listing of latest assignments.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Title</TableHead>
          <TableHead>Topic</TableHead>
          <TableHead>Class</TableHead>
          <TableHead>Instructor</TableHead>
          <TableHead>Due Date</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {assignments.map((project) => (
          <TableRow key={project.$id}>
            <TableCell>{project.title}</TableCell>
            <TableCell>{project.topic}</TableCell>
            <TableCell>{project.className}</TableCell>
            <TableCell>{project.instructor}</TableCell>
            <TableCell>{project.dueDate}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Desk>
  )
}

This file defines a React part, AssignmentsTables, which renders a desk to show a listing of assignments. It takes an array of assignments as props and maps via them to populate the desk rows with particulars like title, topic, class, instructor, and due date. The desk features a caption and headers for higher readability.

import kind React from "react"

interface AuthLayoutProps {
    kids: React.ReactNode
    title: string
    description?: string
}

export perform AuthLayout({ kids, title, description }: AuthLayoutProps) {
    return (
        <div className="min-h-screen grid lg:grid-cols-2">
            {}
            <div className="flex items-center justify-center p-8">
                <div className="mx-auto w-full max-w-sm space-y-6">
                    <div className="space-y-2 text-center">
                        <h1 className="text-3xl font-bold tracking-tight">{title}</h1>
                        {description && <p className="text-sm text-muted-foreground">{description}</p>}
                    </div>
                    {kids}
                </div>
            </div>

            {}
            <div className="hidden lg:block relative bg-black">
                <div className="absolute inset-0 bg-[url('https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-xOOAKcDxPyvxlDygdNGtUvjEA6QHBO.png')] bg-cover bg-center opacity-50" />
                <div className="relative h-full flex items-center justify-center text-white p-12">
                    <div className="space-y-6 max-w-lg">
                        <h2 className="text-4xl font-bold">Maintain Your Kids's Success</h2>
                        <p className="text-lg text-gray-200">
                            Join with lecturers, observe progress, and keep concerned in your kid's schooling journey.
                        </p>
                    </div>
                </div>
            </div>
        </div>
    )
}

This file defines a React part, AuthLayout, which gives a format for authentication pages. It features a left facet for kinds (with a title and elective description) and a proper facet with a background picture and motivational textual content. The format is responsive, hiding the picture on smaller screens.

import { E book, BarChart, MessageCircle } from "lucide-react"

const options = [
  {
    name: "Comprehensive Dashboard",
    description: "View student's overall academic performance, including average grades and progress over time.",
    icon: BarChart,
  },
  {
    name: "Easy Communication",
    description: "Direct messaging system between school administrators and teachers for quick and efficient communication.",
    icon: MessageCircle,
  },
  {
    name: "Academic Tracking",
    description:
      "Monitor assignments, upcoming tests, and project deadlines to help your students stay on top of their studies.",
    icon: Book,
  },
]

export perform Options() {
  return (
    <div className="py-12 bg-white" id="options">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="lg:text-center">
          <h2 className="text-base text-primary font-semibold tracking-wide uppercase">Options</h2>
          <p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
            All the pieces you should keep related
          </p>
          <p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
            Our platform presents a spread of options designed to boost communication between faculty directors and lecturers.
          </p>
        </div>

        <div className="mt-10">
          <dl className="space-y-10 md:space-y-0 md:grid md:grid-cols-3 md:gap-x-8 md:gap-y-10">
            {options.map((characteristic) => (
              <div key={characteristic.identify} className="relative">
                <dt>
                  <div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-primary text-white">
                    <characteristic.icon className="h-6 w-6" aria-hidden="true" />
                  </div>
                  <p className="ml-16 text-lg leading-6 font-medium text-gray-900">{characteristic.identify}</p>
                </dt>
                <dd className="mt-2 ml-16 text-base text-gray-500">{characteristic.description}</dd>
              </div>
            ))}
          </dl>
        </div>
      </div>
    </div>
  )
}

This file defines a React part, Options, which showcases key platform options in a visually interesting format. It features a title, description, and a grid of characteristic playing cards, every with an icon, identify, and detailed description. The part is designed to focus on the platform’s capabilities for college directors and lecturers.

This file defines a React part, Footer, which shows a easy footer with social media icons (Fb and Twitter) and a copyright discover. The footer is centered and responsive, with social hyperlinks on the correct and the copyright textual content on the left for bigger screens.

This file defines a React part, Hero, which creates a visually partaking hero part for an internet site. It features a daring headline, a descriptive paragraph, and two call-to-action buttons (“Get began” and “Be taught extra”). The format encompasses a responsive design with a background form and a picture on the correct facet for bigger screens.

This file defines a React part, MobileMenu, which creates a responsive cellular navigation menu. It toggles visibility with a button and contains hyperlinks to options, about, and get in touch with sections, in addition to login and sign-up buttons. The menu is styled with a clear, fashionable design and closes when clicking the shut icon.

This file defines a React part, Navbar, which creates a responsive navigation bar with hyperlinks to options, about, and get in touch with sections. It contains login and sign-up buttons for bigger screens and integrates a MobileMenu part for smaller screens. The navbar is styled with a shadow and a centered format.

  • NotAuthorizedDialog.tsx file:

This file defines a React part, NotAuthorizedDialog, which shows a dialog when a consumer will not be licensed to carry out an motion. It features a title and outline prompting the consumer to contact an administrator, and its visibility is managed by way of the open and onOpenChange props.

This file defines a React part, StudentsTables, which renders a desk to show a listing of scholars. It takes an array of scholars as props and maps via them to populate the desk rows with particulars like first identify, final identify, class, gender, and age. The desk features a caption and headers for higher readability.

Confer with the GitHub code for the respective code of the parts talked about above.

State administration and kinds

Now for the following step, we’ll be creating the state and kinds we’ll be utilizing all through the appliance. Create the retailer and types folders within the root of the venture folder.

  • Inside the shop folder, create the next information and paste the corresponding code:
import { create } from "zustand"
import { persist } from "zustand/middleware"

interface Person {
  $id: string
  firstName: string
  lastName: string
  e-mail: string
}

interface AuthState  null;
  setToken: (token: string 

export const useAuthStore = create()(
  persist(
    (set) => ({
      consumer: null,
      setUser: (consumer) => set({ consumer }),
      token: null,
      setToken: (token) => set({ token }),
      logout: () => set({ consumer: null }),
    }),
    {
      identify: "auth-storage", // Persist state in localStorage
    }
  )
)

This file defines a Zustand retailer, useAuthStore, for managing authentication state. It contains consumer and token states, together with strategies to set the consumer, set the token, and sign off. The state is endured in localStorage utilizing the persist middleware.

import { create } from "zustand";
import { persist } from "zustand/middleware";

interface Profile {
  firstName: string;
  lastName: string;
  e-mail: string;
  position: string;
  userId: string;
  $id: string;
  $createdAt: string;
}

interface ProfileStore  null;
  setProfile: (profile: Profile) => void;
  clearProfile: () => void;


export const useProfileStore = create<ProfileStore>()(
  persist(
    (set) => ({
      profile: null,
      setProfile: (profile) => set({ profile }),
      clearProfile: () => set({ profile: null }),
    }),
    {
      identify: "profile-storage", 
    }
  )
);

This file defines a Zustand retailer, useProfileStore, for managing consumer profile knowledge. It features a profile state and strategies to set and clear the profile. The state is endured in localStorage utilizing the persist middleware.

  • Inside the kinds folder, create the next file and paste the next code within the index.ts file:
export interface Task {
  title: string;
  topic: string;
  className: string;
  instructor: string;
  dueDate: string;
  creatorEmail: string;
}

export interface AssignmentsTable extends Task {
  $id: string;
  }

export interface Pupil {
  firstName: string;
  lastName: string;
  gender: string;
  className: string;
  age: quantity;
  creatorEmail: string;
}

export interface StudentsTable extends Pupil {
  $id: string;
}

This file defines TypeScript interfaces for Task, AssignmentsTable, Pupil, and StudentsTable. It extends the bottom Task and Pupil interfaces with further properties like $id for database data, guaranteeing constant typing throughout the appliance.

Routes

Now we get to see how the parts and retailer we simply created are getting used within the software.

Change the code within the app/web page.tsx file with the code under:

import { Navbar } from "@/parts/Navbar"
import { Hero } from "@/parts/Hero"
import { Options } from "@/parts/Options"
import { Footer } from "@/parts/Footer"

export default perform Dwelling() {
  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <fundamental className="flex-grow">
        <Hero />
        <Options />
      </fundamental>
      <Footer />
    </div>
  )
}

This file defines the principle dwelling web page part, which constructions the format utilizing Navbar, Hero, Options, and Footer parts. It ensures a responsive design with a flex format and full-page peak.

Create the next folders within the app folder and paste this code of their respective web page.tsx information:

  • Create a signup folder and paste this code in its web page.tsx file:
"use consumer"

import { useState } from "react"
import Hyperlink from "subsequent/hyperlink"
import { useRouter } from "subsequent/navigation"
import { Button } from "@/parts/ui/button"
import { Enter } from "@/parts/ui/enter"
import { Label } from "@/parts/ui/label"
import { AuthLayout } from "@/parts/auth-layout"
import { useAuthStore } from "@/retailer/auth" 

export default perform SignupPage() {
  const router = useRouter()
  const { setUser, setToken } = useAuthStore() 
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async perform onSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    const formData = new FormData(e.currentTarget as HTMLFormElement);
    const userData = {
        identify: `${formData.get("firstName")} ${formData.get("lastName")}`,
        e-mail: formData.get("e-mail"),
        password: formData.get("password"),
    };

    attempt {
        const response = await fetch("https://edtech-saas-backend.vercel.app/api/auth/signup", {
            technique: "POST",
            headers: { "Content material-Sort": "software/json" },
            physique: JSON.stringify(userData),
        });

        const end result = await response.json();

        if (!response.okay || !end result.success) {
            throw new Error("Signup failed. Please attempt once more.");
        }

        console.log("Signup profitable:", end result);

        
        const [firstName, ...lastNameParts] = end result.consumer.identify.cut up(" ");
        const lastName = lastNameParts.be a part of(" ") || ""; 

        
        setUser({
            $id: end result.consumer.$id,
            firstName,
            lastName,
            e-mail: end result.consumer.e-mail,
        });
        setToken(end result.token);
        console.log("Person:", end result.consumer);
        console.log("Token:", end result.token)
        router.push("/role-selection");
    } catch (err)  lastly {
        setIsLoading(false);
    }
}

  return (
    <AuthLayout title="Create an account" description="Enter your particulars to get began">
      <type onSubmit={onSubmit} className="space-y-4">
        <div className="grid gap-4 grid-cols-2">
          <div className="space-y-2">
            <Label htmlFor="firstName">First identify</Label>
            <Enter identify="firstName" id="firstName" placeholder="John" disabled={isLoading} required />
          </div>
          <div className="space-y-2">
            <Label htmlFor="lastName">Final identify</Label>
            <Enter identify="lastName" id="lastName" placeholder="Doe" disabled={isLoading} required />
          </div>
        </div>
        <div className="space-y-2">
          <Label htmlFor="e-mail">Electronic mail</Label>
          <Enter identify="e-mail" id="e-mail" placeholder="identify@instance.com" kind="e-mail" autoComplete="e-mail" disabled={isLoading} required />
        </div>
        <div className="space-y-2">
          <Label htmlFor="password">Password</Label>
          <Enter identify="password" id="password" kind="password" disabled={isLoading} required />
        </div>
        {error && <p className="text-red-500 text-sm">{error}</p>}
        <Button className="w-full" kind="submit" disabled={isLoading}>
          {isLoading ? "Creating account..." : "Create account"}
        </Button>
      </type>
      <div className="text-center text-sm">
        <Hyperlink href="/login" className="underline underline-offset-4 hover:text-primary">
          Have already got an account? Signal in
        </Hyperlink>
      </div>
    </AuthLayout>
  )
}

This file defines a SignupPage part for consumer registration, dealing with type submission with validation and error dealing with. It makes use of Zustand to retailer consumer knowledge and a token upon profitable signup, then redirects to a task choice web page. The shape contains fields for first identify, final identify, e-mail, and password, with a hyperlink to the login web page for present customers.

  • Create a role-selection folder and paste this code in its web page.tsx file:
"use consumer"

import { useState } from "react"
import { useRouter } from "subsequent/navigation"
import { Button } from "@/parts/ui/button"
import { Card, CardContent } from "@/parts/ui/card"
import { GraduationCap, Customers } from "lucide-react"
import { useAuthStore } from "@/retailer/auth"
import { useProfileStore } from "@/retailer/profile"

const roles = [
  {
      id: "Admin",
      title: "Admin",
      description: "Manage teachers, classes, and more",
      icon: GraduationCap,
  },
  {
    id: "Teacher",
    title: "Teacher",
    description: "Access your class dashboard, manage grades, and communicate with students",
    icon: GraduationCap,
  },
  {
    id: "Student",
    title: "Student",
    description: "Monitor your progress and communicate with teachers",
    icon: Users,
  },
]

export default perform RoleSelectionPage() {
  const { consumer, token } = useAuthStore()
  const { setProfile } = useProfileStore()
  console.log("Person:", consumer);
  const router = useRouter()
  const [selectedRole, setSelectedRole] = useState<string | null>(null)
  console.log("Chosen Function:", selectedRole);
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async perform onSubmit(e: React.FormEvent) {
    e.preventDefault()
    if (!selectedRole || !consumer) return
    setIsLoading(true)
    setError(null)

    const formattedRole =
      selectedRole.charAt(0).toUpperCase() + selectedRole.slice(1).toLowerCase(); 

    const payload = {
      firstName: consumer?.firstName,
      lastName: consumer?.lastName,
      e-mail: consumer?.e-mail,
      position: formattedRole,
      userId: consumer?.$id,
    }
    console.log("Payload", payload)

    attempt {
      const response = await fetch("https://edtech-saas-backend.vercel.app/api/profile", {
        technique: "POST",
        headers: {
          "Authorization": `Bearer ${token}`,
          "Content material-Sort": "software/json"
        },
        physique: JSON.stringify(payload),
      })

      const knowledge = await response.json()
      if (!response.okay)  "Didn't create profile")
      
      console.log("Profile Information", knowledge)
      setProfile({
        firstName: knowledge?.consumer?.firstName,
        lastName: knowledge?.consumer?.lastName,
        e-mail: knowledge?.consumer?.e-mail,
        position: knowledge?.consumer?.position,
        userId: knowledge?.consumer?.userId,
        $id: knowledge?.consumer?.$id,
        $createdAt: knowledge?.consumer?.$createdAt,
      })
      router.push("/dashboard")
    } catch (err) {
      const error = err as Error
      setError(error.message)
      console.error("Error:", error)
    } lastly {
      setIsLoading(false)
    }
  }

  return (
      <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
        <div className="max-w-md w-full space-y-8">
          <div className="text-center space-y-2">
            <h1 className="text-3xl font-bold">Choose your position</h1>
            <p className="text-gray-500">Select your position to entry the suitable dashboard</p>
          </div>
          {error && <p className="text-red-500 text-center">{error}</p>}
          <type onSubmit={onSubmit} className="space-y-4">
            <div className="grid gap-4">
              {roles.map((position) => {
                const Icon = position.icon
                return (
                    <Card
                    key={position.id}
                    className={`cursor-pointer transition-colors ${selectedRole === position.id ? "border-black" : ""}`}
                    onClick={() => setSelectedRole(position.title)}
                    >
                      <CardContent className="flex items-start gap-4 p-6">
                        <div className="rounded-full p-2 bg-gray-100">
                          <Icon className="h-6 w-6" />
                        </div>
                        <div className="space-y-1">
                          <h3 className="font-medium">{position.title}</h3>
                          <p className="text-sm text-gray-500">{position.description}</p>
                        </div>
                      </CardContent>
                    </Card>
                )
              })}
            </div>

            <Button className="w-full" kind="submit" disabled=>
              {isLoading ? "Confirming..." : "Proceed"}
            </Button>
          </type>
        </div>
      </div>
  )
}

This file defines a RoleSelectionPage part the place customers choose their position (Admin, Instructor, or Pupil) after signing up. It handles position choice, submits the info to create a profile, and redirects to the dashboard upon success. The UI contains playing cards for every position, a affirmation button, and error dealing with.

  • Create a login folder and paste this code in its web page.tsx file:
"use consumer";

import { useState } from "react";
import Hyperlink from "subsequent/hyperlink";
import { useRouter } from "subsequent/navigation";
import { Button } from "@/parts/ui/button";
import { Enter } from "@/parts/ui/enter";
import { Label } from "@/parts/ui/label";
import { AuthLayout } from "@/parts/auth-layout";
import { useAuthStore } from "@/retailer/auth";
import { useProfileStore } from "@/retailer/profile";

export default perform LoginPage() {
  const router = useRouter();
  const [isLoading, setIsLoading] = useState(false);
  const { setUser, setToken } = useAuthStore() 
  const [formData, setFormData] = useState({ e-mail: "", password: "" });
  const [error, setError] = useState<string | null>(null)

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ ...formData, [e.target.name]: e.goal.worth });
  };

  async perform onSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsLoading(true);
    setError(null);
 
    console.log("FormData", formData);
 
    attempt {
      
      const authResponse = await fetch("https://edtech-saas-backend.vercel.app/api/auth/login", {
        technique: "POST",
        headers: {
          "Content material-Sort": "software/json",
        },
        physique: JSON.stringify(formData),
      });
 
      if (!authResponse.okay) throw new Error("Invalid credentials");
 
      const authData = await authResponse.json();
      console.log("Auth End result:", authData);
 
      const token = authData.token;
      setToken(token);
 
      setUser({
        $id: authData.session.$id,
        firstName: "",
        lastName: "",
        e-mail: authData.session.providerUid,
      });
 
      
      const profileResponse = await fetch(`https://edtech-saas-backend.vercel.app/api/profile/${formData.e-mail}`, {
        technique: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content material-Sort": "software/json",
        },
      });
 
      if (!profileResponse.okay) throw new Error("Didn't fetch consumer profile");
 
      const profileData = await profileResponse.json();
      console.log("Profile Information:", profileData);
 
      if (profileData.profile) {
        
        useProfileStore.getState().setProfile(profileData.profile);
        router.push("/dashboard");
      } else {
        router.push("/role-selection");
      }
    } catch (err)  "An error occurred");
     lastly {
      setIsLoading(false);
    }
  }
 

  return (
    <AuthLayout title="Welcome again" description="Enter your credentials to entry your account">
      <type onSubmit={onSubmit} className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="e-mail">Electronic mail</Label>
          <Enter
            id="e-mail"
            identify="e-mail"
            placeholder="identify@instance.com"
            kind="e-mail"
            autoCapitalize="none"
            autoComplete="e-mail"
            autoCorrect="off"
            disabled={isLoading}
            required
            onChange={handleChange}
          />
        </div>
        <div className="space-y-2">
          <Label htmlFor="password">Password</Label>
          <Enter
            id="password"
            identify="password"
            kind="password"
            disabled={isLoading}
            required
            onChange={handleChange}
          />
        </div>
        {error && <p className="text-red-500 text-sm">{error}</p>}
        <Button className="w-full" kind="submit" disabled={isLoading}>
          {isLoading ? "Signing in..." : "Sign up"}
        </Button>
      </type>
      <div className="text-center text-sm">
        <Hyperlink href="/signup" className="underline underline-offset-4 hover:text-primary">
          Do not have an account? Enroll
        </Hyperlink>
      </div>
    </AuthLayout>
  );
}

This file defines a LoginPage part for consumer authentication, dealing with type submission with e-mail and password. It makes use of Zustand to retailer consumer knowledge and a token, fetches the consumer’s profile, and redirects to the dashboard or position choice web page primarily based on the profile standing. The shape contains error dealing with and a hyperlink to the signup web page for brand new customers.

  • Create a dashboard folder and paste this code in its web page.tsx file:
"use consumer";

import { useState, useEffect } from "react";
import { StudentsTables } from "@/parts/StudentsTable";
import { Button } from "@/parts/ui/button";
import { NotAuthorizedDialog } from "@/parts/NotAuthorizedDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/parts/ui/tabs";
import { useAuthStore } from "@/retailer/auth";
import { useProfileStore } from "@/retailer/profile";
import { AddStudentDialog } from "@/parts/AddStudentDialog";
import { AddAssignmentDialog } from "@/parts/AddAssignmentDialog";
import {Task,  AssignmentsTable, Pupil, StudentsTable } from "@/sorts";
import { AssignmentsTables } from "@/parts/AssignmentsTable";
import axios from "axios";

export default perform TeacherDashboard() {
  const { token, logout } = useAuthStore();
  const { profile, clearProfile } = useProfileStore();
  const [isNotAuthorizedDialogOpen, setIsNotAuthorizedDialogOpen] = useState(false);
  const [isAddStudentDialogOpen, setIsAddStudentDialogOpen] = useState(false);
  const [isAddAssignmentDialogOpen, setIsAddAssignmentDialogOpen] = useState(false);
  const [students, setStudents] = useState<StudentsTable[]>([]);
  const [assignments, setAssignments] = useState<AssignmentsTable[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");
 

  const API_URL_STUDENTS = "https://edtech-saas-backend.vercel.app/api/college students";
  const API_URL_ASSIGNMENTS = "https://edtech-saas-backend.vercel.app/api/assignments/create";

 

  async perform fetchData() {
    setLoading(true);
    setError("");
 
    const headers = {
      "Content material-Sort": "software/json",
      Authorization: `Bearer ${token}`,
    };
 
    const e-mail = profile?.e-mail;
    if (!e-mail) {
      setError("Electronic mail is required");
      return;
    }
 
    
    const studentsUrl = `https://edtech-saas-backend.vercel.app/api/college students/${e-mail}`;
    const assignmentsUrl = `https://edtech-saas-backend.vercel.app/api/assignments/${e-mail}`;
 
    
    attempt {
      const studentsRes = await axios.get(studentsUrl, { headers });
      console.log("College students Information:", studentsRes.knowledge);
      setStudents(studentsRes.knowledge);
    } catch (err) {
      console.warn("Didn't fetch college students knowledge:", err);
      setStudents([]); 
    }
 
    
    attempt {
      const assignmentsRes = await axios.get(assignmentsUrl, { headers });
      console.log("Assignments Information:", assignmentsRes.knowledge);
      setAssignments(assignmentsRes.knowledge);
    } catch (err) {
      console.error("Error fetching assignments knowledge:", err);
      setError((err as Error).message);
    } lastly {
      setLoading(false);
    }
  }
 
 
 
  useEffect(() => {
    if (!token) return;

    fetchData();
  }, [token]);

    const handleAddStudent = async (knowledge: Omit<Pupil, 'creatorEmail'>) => {
    setLoading(true);
    setError("");
 
    const payload = {
      firstName: knowledge.firstName,
      lastName: knowledge.lastName,
      gender: knowledge.gender,
      className: knowledge.className,
      age: knowledge.age,
      creatorEmail: profile?.e-mail,
    };
    console.log("College students payload:", payload);
 
    attempt {
      const response = await fetch(API_URL_STUDENTS, {
        technique: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content material-Sort": "software/json",
        },
        physique: JSON.stringify(payload),
      });
 
      const end result = await response.json(); 
      console.log("Pupil End result", end result);
 
      if (response.standing === 403 && end result.message === "Not licensed") {
        setIsAddStudentDialogOpen(false);
        setIsNotAuthorizedDialogOpen(true);
        return; 
      }
 
      if (!response.okay) throw new Error(end result.message || "Failed so as to add pupil");
 
      setStudents((prevStudents: Pupil[]) => [...prevStudents, result]); 
      setIsAddStudentDialogOpen(false);
      await fetchData();
    } catch (err) {
      if ((err as Error & { code?: quantity }).code === 403 && (err as Error).message === "Not licensed") {
        setIsAddStudentDialogOpen(false);
        setIsNotAuthorizedDialogOpen(true);
        return;
      }
      setError((err as Error).message);
      console.error("Error:", err);
    } lastly {
      setLoading(false);
    }
  };
 

    const handleAddAssignment = async (knowledge: Task) => {
    setLoading(true);
    setError("");
 
    const payload = {
      title: knowledge.title,
      topic: knowledge.topic,
      className: knowledge.className,
      instructor: knowledge.instructor,
      dueDate: knowledge.dueDate,
      creatorEmail: profile?.e-mail,
    };
 
    attempt {
      const response = await fetch(API_URL_ASSIGNMENTS, {
        technique: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content material-Sort": "software/json",
        },
        physique: JSON.stringify(payload),
      });
 
      const end result = await response.json(); 
 
      if (response.standing === 403 && end result.message === "Not licensed") {
        setIsAddAssignmentDialogOpen(false);
        setIsNotAuthorizedDialogOpen(true);
        return; 
      }
 
      if (!response.okay) throw new Error(end result.message || "Failed so as to add project");
 
      setAssignments((prevAssignments: Task[]) => [...prevAssignments, result]); 
      setIsAddAssignmentDialogOpen(false);
    } catch (err) {
      if ((err as Error & { code?: quantity }).code === 403 && (err as Error).message === "Not licensed") {
        setIsAddAssignmentDialogOpen(false);
        setIsNotAuthorizedDialogOpen(true);
        return;
      }
      setError((err as Error).message);
      console.error("Error:", err);
    } lastly {
      setLoading(false);
    }
  };

  const handleLogout = () => {
    clearProfile();
    logout();
    window.location.href = "/login";
  };

  return (
    <div className="container mx-auto p-4">
      <div className="flex items-center justify-between mb-4">
        <div>
          <h1 className="text-2xl font-bold mb-2">Welcome {profile?.firstName}</h1>
          <p className="text-gray-600 mb-6">
            You're logged in as {profile?.position === "Admin" ? "an" : "a"} {profile?.position}.
          </p>
        </div>
        <Button variant="default" onClick={handleLogout}>Log off</Button>
      </div>

      {profile?.position === 'Pupil'
      ?  (
        <div>
          <AssignmentsTables assignments={assignments} />
        </div>
        )
        : (
          <Tabs defaultValue="college students" className="w-full">
            <TabsList className="grid w-full grid-cols-2">
              <TabsTrigger worth="college students">College students</TabsTrigger>
              <TabsTrigger worth="assignments">Assignments</TabsTrigger>
            </TabsList>

            <TabsContent worth="college students">
              <StudentsTables college students={college students} />
              <Button onClick={() => setIsAddStudentDialogOpen(true)}>Add a Pupil</Button>
            </TabsContent>

            <TabsContent worth="assignments">
              <AssignmentsTables assignments={assignments} />
              <Button onClick={() => setIsAddAssignmentDialogOpen(true)}>Add Task</Button>
            </TabsContent>
          </Tabs>
        )}

      {error && <p className="text-red-500 mt-4">{error}</p>}

      <NotAuthorizedDialog open={isNotAuthorizedDialogOpen} onOpenChange={setIsNotAuthorizedDialogOpen} />
      <AddStudentDialog creatorEmail= "" loading={loading} open={isAddStudentDialogOpen} onOpenChange={setIsAddStudentDialogOpen} onAddStudent={handleAddStudent} />
      <AddAssignmentDialog creatorEmail= "" open={isAddAssignmentDialogOpen} onOpenChange={setIsAddAssignmentDialogOpen} onAddAssignment={handleAddAssignment} />
    </div>
  );
}

This file defines a TeacherDashboard part that shows a dashboard for lecturers or admins, permitting them to handle college students and assignments. It contains tabs for switching between college students and assignments, buttons so as to add new entries, and handles authorization errors. The part fetches and shows knowledge primarily based on the consumer’s position, with a logout choice and error dealing with.

After creating all of the information and parts above and utilizing them as I’ve proven you, your software ought to work while you run this command under:

The app will probably be accessible at http://localhost:3000/.

Check out the appliance now by creating a faculty, signing up and logging in as an admin, instructor or pupil, and performing some actions.

Constructing a multi-tenant EdTech SaaS software with Subsequent.js, Appwrite, and Allow supplied a number of insights into authorization, safety, and scalability. Listed here are the important thing takeaways:

  • Simplified Function-Primarily based Entry Management (RBAC): With Allow, defining and imposing admin, instructor, and pupil roles was easy. As an alternative of hardcoding permissions, I may dynamically handle them by way of the Allow UI.
  • Allow’s tenant-aware insurance policies ensured that colleges (tenants) remained remoted from each other. This was necessary for knowledge safety in a multi-tenant SaaS app.
  • As an alternative of writing and managing customized permission logic throughout dozens of API routes, Allow dealt with entry management in a centralized solution to cut back complexity and make future updates simpler.
  • Since all authorization checks had been enforced on the backend, the frontend solely displayed UI parts primarily based on permissions, guaranteeing a clean consumer expertise.
  • Implementing customized authentication from scratch may have taken weeks. However utilizing Appwrite for authentication and Allow for authorization, I used to be capable of concentrate on constructing core options as an alternative of reinventing entry management.

Conclusion

Integrating Allow with Subsequent.js & Appwrite enabled me to simplify authorization in my multi-tenant Edtech SaaS software. By offloading advanced permission logic to Allow, I used to be capable of concentrate on constructing options, not managing entry management manually.

For those who’re constructing a SaaS app with advanced permissions & multi-tenancy, Allow is a superb software to make use of to streamline your workflow.

Entry the GitHub repo of the completed venture for the backend right here and the frontend right here.

Tags: ApplicationBuildFrontendIntegrationMultiTenantNext.jsSaaSSitePoint
Admin

Admin

Next Post
The best way to Use AI for Writing Distinctive Content material (7 Finest Practices)

The best way to Use AI for Writing Distinctive Content material (7 Finest Practices)

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

SquareX to Uncover Knowledge Splicing Assaults at BSides San Francisco, A Main DLP Flaw that Compromises Knowledge Safety of Tens of millions

SquareX to Uncover Knowledge Splicing Assaults at BSides San Francisco, A Main DLP Flaw that Compromises Knowledge Safety of Tens of millions

April 16, 2025
A SQL MERGE assertion performs actions primarily based on a RIGHT JOIN

Easy methods to Integration Take a look at Saved Procedures with jOOQ – Java, SQL and jOOQ.

May 22, 2025

Trending.

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

April 10, 2025
Expedition 33 Guides, Codex, and Construct Planner

Expedition 33 Guides, Codex, and Construct Planner

April 26, 2025
How you can open the Antechamber and all lever places in Blue Prince

How you can open the Antechamber and all lever places in Blue Prince

April 14, 2025
Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

April 28, 2025
Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

May 5, 2025

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

How To Drive Extra Conversions With Fewer Clicks [MozCon 2025 Speaker Series]

How To Drive Extra Conversions With Fewer Clicks [MozCon 2025 Speaker Series]

June 18, 2025
FedRAMP at Startup Velocity: Classes Discovered

FedRAMP at Startup Velocity: Classes Discovered

June 18, 2025
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved