feat/deliverables #22
@ -14,6 +14,7 @@ import {
|
|||||||
Bell,
|
Bell,
|
||||||
Settings,
|
Settings,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
@ -98,6 +99,19 @@ export function Header() {
|
|||||||
<Calendar className="w-4 h-4 sm:w-5 sm:h-5" />
|
<Calendar className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||||
<span className="hidden sm:inline">Book Appointment</span>
|
<span className="hidden sm:inline">Book Appointment</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/deliverables"
|
||||||
|
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 md:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors ${
|
||||||
|
pathname === "/deliverables"
|
||||||
|
? "bg-linear-to-r from-rose-500 to-pink-600 text-white"
|
||||||
|
: isDark
|
||||||
|
? "text-gray-300 hover:bg-gray-800"
|
||||||
|
: "text-gray-600 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||||
|
<span className="hidden sm:inline">Documentation</span>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Right Side Actions */}
|
{/* Right Side Actions */}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
Heart,
|
Heart,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
@ -20,6 +21,7 @@ import { toast } from "sonner";
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
|
{ label: "Dashboard", icon: LayoutGrid, href: "/admin/dashboard" },
|
||||||
{ label: "Book Appointment", icon: Calendar, href: "/admin/booking" },
|
{ label: "Book Appointment", icon: Calendar, href: "/admin/booking" },
|
||||||
|
{ label: "Deliverables", icon: FileText, href: "/deliverables" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SideNav() {
|
export default function SideNav() {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ArrowDownRight,
|
ArrowDownRight,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
import { getAllUsers } from "@/lib/actions/auth";
|
import { getAllUsers } from "@/lib/actions/auth";
|
||||||
@ -242,7 +245,17 @@ export default function Dashboard() {
|
|||||||
Here's an overview of your practice today
|
Here's an overview of your practice today
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Select value={timePeriod} onValueChange={setTimePeriod}>
|
<div className="flex items-center gap-3">
|
||||||
|
<Link href="/deliverables">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={`flex items-center gap-2 ${isDark ? "bg-gray-800 border-gray-700 text-gray-100 hover:bg-gray-700" : "bg-white border-gray-200 text-gray-900 hover:bg-gray-50"}`}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Deliverables</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Select value={timePeriod} onValueChange={setTimePeriod}>
|
||||||
<SelectTrigger className={`w-full sm:w-[200px] cursor-pointer ${isDark ? "bg-gray-800 border-gray-700 text-gray-100" : "bg-white border-gray-200 text-gray-900"}`}>
|
<SelectTrigger className={`w-full sm:w-[200px] cursor-pointer ${isDark ? "bg-gray-800 border-gray-700 text-gray-100" : "bg-white border-gray-200 text-gray-900"}`}>
|
||||||
<SelectValue placeholder="Select period" />
|
<SelectValue placeholder="Select period" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -252,6 +265,7 @@ export default function Dashboard() {
|
|||||||
<SelectItem className={isDark ? "focus:bg-gray-700" : ""} value="last_year">Last Year</SelectItem>
|
<SelectItem className={isDark ? "focus:bg-gray-700" : ""} value="last_year">Last Year</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@ -375,33 +375,14 @@ function LoginContent() {
|
|||||||
{/* Heading */}
|
{/* Heading */}
|
||||||
<h1 className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
|
<h1 className="text-3xl font-bold bg-linear-to-r from-rose-600 via-pink-600 to-rose-600 bg-clip-text text-transparent mb-2">
|
||||||
{step === "login" && "Welcome back"}
|
{step === "login" && "Welcome back"}
|
||||||
{step === "signup" && "Create an account"}
|
|
||||||
{step === "verify" && "Verify your email"}
|
{step === "verify" && "Verify your email"}
|
||||||
</h1>
|
</h1>
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
{step === "login" && (
|
{step === "login" && (
|
||||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||||
New to Attune Heart Therapy?{" "}
|
Sign in to access your admin dashboard
|
||||||
<Link
|
|
||||||
href="/signup"
|
|
||||||
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{step === "signup" && (
|
|
||||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
|
||||||
Already have an account?{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setStep("login")}
|
|
||||||
className={`underline font-medium ${isDark ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
|
|
||||||
>
|
|
||||||
Log in
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{step === "verify" && registeredEmail && (
|
{step === "verify" && registeredEmail && (
|
||||||
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
<p className={`mb-6 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>
|
||||||
We've sent a verification code to <strong>{registeredEmail}</strong>
|
We've sent a verification code to <strong>{registeredEmail}</strong>
|
||||||
@ -519,168 +500,6 @@ function LoginContent() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Signup Form */}
|
|
||||||
{step === "signup" && (
|
|
||||||
<form className="space-y-4" onSubmit={handleSignup}>
|
|
||||||
{/* First Name Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="firstName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
First Name *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="firstName"
|
|
||||||
type="text"
|
|
||||||
placeholder="John"
|
|
||||||
value={signupData.first_name}
|
|
||||||
onChange={(e) => handleSignupChange("first_name", e.target.value)}
|
|
||||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.first_name ? 'border-red-500' : ''}`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{errors.first_name && (
|
|
||||||
<p className="text-sm text-red-500">{errors.first_name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Last Name Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="lastName" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
Last Name *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="lastName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Doe"
|
|
||||||
value={signupData.last_name}
|
|
||||||
onChange={(e) => handleSignupChange("last_name", e.target.value)}
|
|
||||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.last_name ? 'border-red-500' : ''}`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{errors.last_name && (
|
|
||||||
<p className="text-sm text-red-500">{errors.last_name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="signup-email" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
Email address *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="signup-email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Email address"
|
|
||||||
value={signupData.email}
|
|
||||||
onChange={(e) => handleSignupChange("email", e.target.value)}
|
|
||||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.email ? 'border-red-500' : ''}`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<p className="text-sm text-red-500">{errors.email}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Phone Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="phone" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
Phone Number (Optional)
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="phone"
|
|
||||||
type="tel"
|
|
||||||
placeholder="+1 (555) 123-4567"
|
|
||||||
value={signupData.phone_number || ""}
|
|
||||||
onChange={(e) => handleSignupChange("phone_number", e.target.value)}
|
|
||||||
className={`h-11 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="signup-password" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
Password *
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="signup-password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
placeholder="Password (min 8 characters)"
|
|
||||||
value={signupData.password}
|
|
||||||
onChange={(e) => handleSignupChange("password", e.target.value)}
|
|
||||||
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password ? 'border-red-500' : ''}`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
|
|
||||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
||||||
>
|
|
||||||
{showPassword ? (
|
|
||||||
<EyeOff className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{errors.password && (
|
|
||||||
<p className="text-sm text-red-500">{errors.password}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Confirm Password Field */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="signup-password2" className={`text-sm font-medium ${isDark ? 'text-gray-300' : 'text-black'}`}>
|
|
||||||
Confirm Password *
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="signup-password2"
|
|
||||||
type={showPassword2 ? "text" : "password"}
|
|
||||||
placeholder="Confirm password"
|
|
||||||
value={signupData.password2}
|
|
||||||
onChange={(e) => handleSignupChange("password2", e.target.value)}
|
|
||||||
className={`h-11 pr-12 ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'} ${errors.password2 ? 'border-red-500' : ''}`}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setShowPassword2(!showPassword2)}
|
|
||||||
className={`absolute right-4 top-1/2 -translate-y-1/2 h-auto w-auto p-0 ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-500 hover:text-gray-700'}`}
|
|
||||||
aria-label={showPassword2 ? "Hide password" : "Show password"}
|
|
||||||
>
|
|
||||||
{showPassword2 ? (
|
|
||||||
<EyeOff className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<Eye className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{errors.password2 && (
|
|
||||||
<p className="text-sm text-red-500">{errors.password2}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
className="w-full h-12 text-base font-semibold bg-linear-to-r from-rose-500 to-pink-600 hover:from-rose-600 hover:to-pink-700 text-white shadow-lg hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-6"
|
|
||||||
>
|
|
||||||
{registerMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
Creating account...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Sign up"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* OTP Verification Form */}
|
{/* OTP Verification Form */}
|
||||||
{step === "verify" && (
|
{step === "verify" && (
|
||||||
@ -775,17 +594,17 @@ function LoginContent() {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Back to signup */}
|
{/* Back to login */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStep("signup");
|
setStep("login");
|
||||||
setOtpData({ email: "", otp: "" });
|
setOtpData({ email: "", otp: "" });
|
||||||
}}
|
}}
|
||||||
className={`text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
|
className={`text-sm font-medium ${isDark ? 'text-gray-400 hover:text-gray-300' : 'text-gray-600 hover:text-gray-700'}`}
|
||||||
>
|
>
|
||||||
← Back to signup
|
← Back to login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
311
app/(pages)/deliverables/page.tsx
Normal file
311
app/(pages)/deliverables/page.tsx
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft, Heart } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import ReactMarkdown, { Components } from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
const ReadmePage = () => {
|
||||||
|
const readmeContent = `
|
||||||
|
## Attune Heart Therapy
|
||||||
|
|
||||||
|
Welcome to your Attune Heart Therapy platform! This documentation provides everything you need to understand and navigate the complete system, including the landing page, booking system, user/client dashboard, and admin dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 What's Included
|
||||||
|
|
||||||
|
Your Attune Heart Therapy platform includes a comprehensive system for managing therapy appointments and client interactions:
|
||||||
|
|
||||||
|
| Section | Description |
|
||||||
|
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Landing Page | Public-facing homepage with navigation, services overview, and booking access |
|
||||||
|
| Booking System | User-friendly appointment booking flow where clients can request therapy sessions |
|
||||||
|
| User Dashboard | Client portal to view appointments, manage profile, and track booking status |
|
||||||
|
| Admin Dashboard | Administrative interface to manage appointments, view statistics, and schedule sessions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Admin Dashboard Access
|
||||||
|
|
||||||
|
### Step 1: Navigate to Login
|
||||||
|
|
||||||
|
1. Go to your website's homepage
|
||||||
|
2. Click on the **"Admin Panel"** link in the footer (under Quick Links)
|
||||||
|
3. Or navigate directly to: \`https://attunehearttherapy.com/login\`
|
||||||
|
|
||||||
|
### Step 2: Login Credentials
|
||||||
|
|
||||||
|
**Email Address:** \`Hello@AttuneHeartTherapy.com\`
|
||||||
|
|
||||||
|
**Password:** \`G&n2S;ffTc8f\`
|
||||||
|
|
||||||
|
### Step 3: Access Dashboard
|
||||||
|
|
||||||
|
1. Enter your admin email address
|
||||||
|
2. Enter your password
|
||||||
|
3. Click **"Sign In"**
|
||||||
|
4. You will be automatically redirected to the Admin Dashboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Quick Access Links
|
||||||
|
|
||||||
|
[Visit Attune Heart Therapy](https://attunehearttherapy.com/) - Official website
|
||||||
|
|
||||||
|
[Access Admin Dashboard](https://attunehearttherapy.com/login) - Login to manage your practice
|
||||||
|
|
||||||
|
[Book an Appointment](https://attunehearttherapy.com/book-now) - Client booking page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support & Contact
|
||||||
|
|
||||||
|
For technical assistance, questions, or issues:
|
||||||
|
|
||||||
|
**Email:** [info@BlackBusinessLabs.com](mailto:info@BlackBusinessLabs.com)
|
||||||
|
|
||||||
|
**Phone:** [(646) 895-4856](tel:+16468954856) - *CEO Tray Bailey's direct mobile*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For questions or additional support, please contact Black Business Labs at the information provided above.*`;
|
||||||
|
|
||||||
|
const components: Components = {
|
||||||
|
h1: ({ node, ...props }) => (
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: "2.2em",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginTop: "1.2em",
|
||||||
|
marginBottom: "0.6em",
|
||||||
|
borderBottom: "1px solid #eaeaea",
|
||||||
|
paddingBottom: "0.3em",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
h2: ({ node, children, ...props }) => {
|
||||||
|
// Extract text content from children
|
||||||
|
const extractText = (child: any): string => {
|
||||||
|
if (typeof child === 'string') return child;
|
||||||
|
if (typeof child === 'number') return String(child);
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
const childProps = child.props as any;
|
||||||
|
if (childProps?.children) {
|
||||||
|
return React.Children.toArray(childProps.children).map(extractText).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const textContent = React.Children.toArray(children).map(extractText).join('');
|
||||||
|
|
||||||
|
// Check if this is the title heading
|
||||||
|
if (textContent.includes('Attune Heart Therapy - System Overview')) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
fontSize: "1.8em",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginTop: "1.2em",
|
||||||
|
marginBottom: "0.6em",
|
||||||
|
borderBottom: "1px solid #eaeaea",
|
||||||
|
paddingBottom: "0.3em",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5em",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
fontSize: "1.8em",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginTop: "1.2em",
|
||||||
|
marginBottom: "0.6em",
|
||||||
|
borderBottom: "1px solid #eaeaea",
|
||||||
|
paddingBottom: "0.3em",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
h3: ({ node, ...props }) => (
|
||||||
|
<h3
|
||||||
|
style={{
|
||||||
|
fontSize: "1.5em",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginTop: "1.2em",
|
||||||
|
marginBottom: "0.6em",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
p: ({ node, ...props }) => (
|
||||||
|
<p style={{ marginBottom: "1.2em", lineHeight: "1.8" }} {...props} />
|
||||||
|
),
|
||||||
|
a: ({ node, ...props }) => (
|
||||||
|
<a
|
||||||
|
style={{ color: "#0366d6", textDecoration: "none", fontWeight: "500" }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
ul: ({ node, ...props }) => (
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
paddingLeft: "1.5em",
|
||||||
|
marginBottom: "1.2em",
|
||||||
|
listStyleType: "disc",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
ol: ({ node, ...props }) => (
|
||||||
|
<ol
|
||||||
|
style={{
|
||||||
|
paddingLeft: "1.5em",
|
||||||
|
marginBottom: "1.2em",
|
||||||
|
listStyleType: "decimal",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
li: ({ node, ...props }) => (
|
||||||
|
<li style={{ marginBottom: "0.4em" }} {...props} />
|
||||||
|
),
|
||||||
|
table: ({ node, ...props }) => (
|
||||||
|
<table
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
borderCollapse: "collapse",
|
||||||
|
marginBottom: "1.2em",
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.08)",
|
||||||
|
border: "1px solid #dfe2e5",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
th: ({ node, ...props }) => (
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
border: "1px solid #dfe2e5",
|
||||||
|
padding: "0.6em 0.8em",
|
||||||
|
textAlign: "left",
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
fontWeight: "600",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
td: ({ node, ...props }) => (
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
border: "1px solid #dfe2e5",
|
||||||
|
padding: "0.6em 0.8em",
|
||||||
|
textAlign: "left",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
pre: ({ node, children, ...props }) => (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#f6f8fa",
|
||||||
|
padding: "1em",
|
||||||
|
borderRadius: "6px",
|
||||||
|
overflowX: "auto",
|
||||||
|
fontSize: "0.9em",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
code: (props) => {
|
||||||
|
// Using `props: any` and casting to bypass TypeScript error with `inline` prop.
|
||||||
|
const {
|
||||||
|
node,
|
||||||
|
inline: isInline,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
// Destructure known non-HTML props from react-markdown to prevent them from being spread onto the <code> tag
|
||||||
|
index,
|
||||||
|
siblingCount,
|
||||||
|
ordered,
|
||||||
|
checked,
|
||||||
|
style: _style, // if style is passed in props, avoid conflict with style object below
|
||||||
|
...htmlProps // Spread remaining props, assuming they are valid HTML attributes for <code>
|
||||||
|
} = props as any;
|
||||||
|
|
||||||
|
const codeStyleBase = {
|
||||||
|
fontFamily:
|
||||||
|
'SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...codeStyleBase,
|
||||||
|
backgroundColor: "rgba(27,31,35,0.07)", // Slightly adjusted for better visibility
|
||||||
|
padding: "0.2em 0.4em",
|
||||||
|
margin: "0 0.1em",
|
||||||
|
fontSize: "85%",
|
||||||
|
borderRadius: "3px",
|
||||||
|
}}
|
||||||
|
{...htmlProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For block code (inside <pre>)
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className={className} // className might contain "language-js" etc.
|
||||||
|
style={{
|
||||||
|
...codeStyleBase,
|
||||||
|
// Most styling for block code is handled by the <pre> wrapper
|
||||||
|
// However, ensure no extra padding/margin if pre handles it
|
||||||
|
padding: 0,
|
||||||
|
backgroundColor: "transparent", // Pre has the background
|
||||||
|
}}
|
||||||
|
{...htmlProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<div className="bg-white p-8 rounded-lg shadow-md">
|
||||||
|
<Button
|
||||||
|
className="bg-gray-100 hover:bg-gray-50 shadow-md text-black"
|
||||||
|
onClick={() => window.history.back()}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2" />
|
||||||
|
</Button>
|
||||||
|
<ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
|
||||||
|
{readmeContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReadmePage;
|
||||||
72
app/(pages)/deliverables/swot/page.tsx
Normal file
72
app/(pages)/deliverables/swot/page.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function SwotAnalysisPage() {
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
// This ensures the PDF viewer only renders on the client side
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link href="/docs">
|
||||||
|
<Button variant="outline" className="mb-4">
|
||||||
|
← Back to Documentation
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-3xl font-bold mb-2">WODEY SWOT Analysis</h1>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
An in-depth analysis of Strengths, Weaknesses, Opportunities, and
|
||||||
|
Threats for the WODEY platform.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mb-4">
|
||||||
|
<a
|
||||||
|
href="/docs/SWOT-Analysis.pdf"
|
||||||
|
download="WODEY-SWOT-Analysis.pdf"
|
||||||
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded shadow-md"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Download PDF
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isClient ? (
|
||||||
|
<div className="w-full h-[calc(100vh-200px)] rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src="/WODEY-SWOT-Analysis.jpg"
|
||||||
|
alt="SWOT Analysis"
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center w-full h-[calc(100vh-200px)] bg-gray-100 rounded-lg">
|
||||||
|
Loading viewer...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -55,7 +55,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
const validation = loginSchema.safeParse(loginData);
|
const validation = loginSchema.safeParse(loginData);
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
const firstError = validation.error.issues[0];
|
const firstError = validation.error.issues[0];
|
||||||
setError(firstError.message);
|
toast.error(firstError.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +64,12 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
|
|
||||||
if (result.tokens && result.user) {
|
if (result.tokens && result.user) {
|
||||||
toast.success("Login successful!");
|
toast.success("Login successful!");
|
||||||
setShowPassword(false);
|
setShowPassword(false);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onLoginSuccess();
|
onLoginSuccess();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : "Login failed. Please try again.";
|
const errorMessage = err instanceof Error ? err.message : "Login failed. Please try again.";
|
||||||
setError(errorMessage);
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -83,7 +82,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
const validation = registerSchema.safeParse(signupData);
|
const validation = registerSchema.safeParse(signupData);
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
const firstError = validation.error.issues[0];
|
const firstError = validation.error.issues[0];
|
||||||
setError(firstError.message);
|
toast.error(firstError.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +105,6 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : "Signup failed. Please try again.";
|
const errorMessage = err instanceof Error ? err.message : "Signup failed. Please try again.";
|
||||||
setError(errorMessage);
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -160,14 +158,9 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div className="overflow-y-auto flex-1 px-6">
|
<div className="overflow-y-auto flex-1 px-6">
|
||||||
{/* Signup Form */}
|
{/* Signup Form */}
|
||||||
{isSignup ? (
|
{isSignup ? (
|
||||||
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleSignup}>
|
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleSignup}>
|
||||||
{error && (
|
|
||||||
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
|
|
||||||
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* First Name Field */}
|
{/* First Name Field */}
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
@ -183,7 +176,7 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
|
className={`h-11 sm:h-12 text-sm sm:text-base ${isDark ? 'bg-gray-700 border-gray-600 text-white placeholder:text-gray-400' : 'bg-white border-gray-300 text-gray-900'}`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Last Name Field */}
|
{/* Last Name Field */}
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
@ -327,11 +320,6 @@ export function LoginDialog({ open, onOpenChange, onLoginSuccess }: LoginDialogP
|
|||||||
) : (
|
) : (
|
||||||
/* Login Form */
|
/* Login Form */
|
||||||
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleLogin}>
|
<form className="space-y-4 sm:space-y-5 py-4 sm:py-6" onSubmit={handleLogin}>
|
||||||
{error && (
|
|
||||||
<div className={`p-3 rounded-lg border ${isDark ? 'bg-red-900/20 border-red-800' : 'bg-red-50 border-red-200'}`}>
|
|
||||||
<p className={`text-sm ${isDark ? 'text-red-200' : 'text-red-800'}`}>{error}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
|
|||||||
@ -29,12 +29,15 @@
|
|||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/hast": "^3.0.4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
877
pnpm-lock.yaml
877
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user