├── src
├── react-app-env.d.ts
├── vite-env.d.ts
├── styles
│ ├── global.css
│ ├── Blog.css
│ ├── AddAppointment.css
│ ├── Appointments.css
│ ├── Register.css
│ ├── Navbar.css
│ ├── Login.css
│ ├── Services.css
│ └── Home.css
├── index.tsx
├── services
│ └── api.ts
├── components
│ ├── ProtectedRoute.tsx
│ └── Navbar.tsx
├── routes.tsx
├── App.tsx
├── pages
│ ├── Blog.tsx
│ ├── Appointments.tsx
│ ├── AddAppointment.tsx
│ ├── Login.tsx
│ ├── Home.tsx
│ ├── Register.tsx
│ └── Services.tsx
└── hooks
│ └── UserContext.tsx
├── .env
├── public
├── Image1.jpg
├── index.html
└── vite.svg
├── tsconfig.json
├── .gitignore
├── vite.config.ts
├── tsconfig.node.json
├── tsconfig.app.json
├── eslint.config.js
├── package.json
└── README.md
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://bance-assetou-mindvision-capstonebackend.onrender.com
--------------------------------------------------------------------------------
/public/Image1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/assetou27/Bance_Assetou_MindVision_CapstoneFrontend/HEAD/public/Image1.jpg
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 |
7 | plugins: [react()],
8 | server: {
9 | proxy: {
10 | "/api": {
11 | target: "http://localhost:5000",
12 | changeOrigin: true,
13 | },
14 | },
15 | },
16 | })
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | /* src/styles/global.css */
2 |
3 | /* Basic reset and global font styles */
4 | body {
5 | margin: 0;
6 | font-family: Arial, sans-serif;
7 | background-color: #f9f9f9;
8 | }
9 |
10 | /* Remove underline from links and use inherited color */
11 | a {
12 | text-decoration: none;
13 | color: inherit;
14 | }
15 |
16 | /* Style for error messages */
17 | .error {
18 | color: red;
19 | }
20 |
21 | /* Style for success messages */
22 | .success {
23 | color: green;
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | // src/index.tsx
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/client';
4 | import App from './App';
5 | // Import global CSS for overall styling (fonts, background, etc.)
6 | import './styles/global.css';
7 |
8 | // Find the root element in the HTML and create a React root
9 | const root = ReactDOM.createRoot(document.getElementById('root')!);
10 |
11 | // Render the App component inside React.StrictMode for extra checks in development
12 | root.render(
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | // src/services/api.ts
2 | import axios from 'axios';
3 |
4 | /**
5 | * Creates an Axios instance for all API calls.
6 | *
7 | * We use an environment variable for the baseURL. If it's not set,
8 | * we fall back to your Render-deployed backend.
9 | *
10 | * If your routes are like "/api/auth", "/api/appointments", etc.,
11 | * then you just need the domain here.
12 | */
13 | const api = axios.create({
14 | baseURL: process.env.REACT_APP_API_URL
15 | || 'https://bance-assetou-mindvision-capstonebackend.onrender.com',
16 | });
17 |
18 | export default api;
19 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 | MindVision Frontend
14 |
15 |
16 | You need to enable JavaScript to run this app.
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | // src/components/ProtectedRoute.tsx
2 | import React, { useContext } from 'react';
3 | import { Navigate } from 'react-router-dom';
4 | import { UserContext } from '../hooks/UserContext';
5 |
6 | // Define props to accept a single child component
7 | interface ProtectedRouteProps {
8 | children: JSX.Element;
9 | }
10 |
11 | export default function ProtectedRoute({ children }: ProtectedRouteProps) {
12 | const { user } = useContext(UserContext);
13 |
14 | // If there is no authenticated user, redirect to the Login page
15 | if (!user) {
16 | return ;
17 | }
18 |
19 | // If the user is authenticated, render the child component
20 | return children;
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/src/styles/Blog.css:
--------------------------------------------------------------------------------
1 | /* src/styles/Blog.css */
2 |
3 | /* Main container for the Blog page */
4 | .blog-page {
5 | max-width: 800px; /* Limit content width */
6 | margin: 0 auto; /* Center horizontally */
7 | padding: 2rem; /* Spacing around content */
8 | font-family: 'Segoe UI', sans-serif;
9 | }
10 |
11 | /* Page title styling */
12 | .blog-page h1 {
13 | text-align: center;
14 | margin-bottom: 1.5rem;
15 | color: #222;
16 | }
17 |
18 | /* Error message styling */
19 | .error {
20 | color: red;
21 | text-align: center;
22 | margin-bottom: 1rem;
23 | }
24 |
25 | /* Grid container for the list of blog posts */
26 | .blog-list {
27 | display: grid;
28 | gap: 1.5rem; /* Space between blog cards */
29 | }
30 |
31 | /* Individual blog card styling */
32 | .blog-card {
33 | background-color: #fff;
34 | border-radius: 6px;
35 | box-shadow: 0 2px 8px rgba(0,0,0,0.1);
36 | padding: 1rem;
37 | }
38 |
39 | /* Blog card title styling */
40 | .blog-card h2 {
41 | margin: 0 0 0.5rem;
42 | color: #333;
43 | }
44 |
45 | /* Blog card text styling */
46 | .blog-card p {
47 | margin: 0.5rem 0;
48 | color: #555;
49 | }
50 |
--------------------------------------------------------------------------------
/src/styles/AddAppointment.css:
--------------------------------------------------------------------------------
1 | /* src/styles/AddAppointment.css */
2 |
3 | /* Container for the add appointment page */
4 | .add-appointment-page {
5 | max-width: 600px;
6 | margin: 0 auto;
7 | padding: 2rem;
8 | font-family: 'Segoe UI', sans-serif;
9 | }
10 |
11 | .add-appointment-page h1 {
12 | text-align: center;
13 | margin-bottom: 1.5rem;
14 | color: #222;
15 | }
16 |
17 | .message {
18 | text-align: center;
19 | margin-bottom: 1rem;
20 | font-weight: bold;
21 | color: green;
22 | }
23 |
24 | /* Appointment form styling */
25 | .appointment-form {
26 | display: flex;
27 | flex-direction: column;
28 | gap: 1rem;
29 | }
30 |
31 | .appointment-form label {
32 | font-weight: 600;
33 | color: #333;
34 | }
35 |
36 | .appointment-form input,
37 | .appointment-form textarea {
38 | padding: 0.75rem;
39 | border: 1px solid #ccc;
40 | border-radius: 6px;
41 | font-size: 1rem;
42 | }
43 |
44 | .appointment-form button {
45 | padding: 0.75rem;
46 | background-color: #6c5ce7;
47 | color: #fff;
48 | font-weight: bold;
49 | border: none;
50 | border-radius: 6px;
51 | cursor: pointer;
52 | transition: background 0.3s;
53 | }
54 |
55 | .appointment-form button:hover {
56 | background-color: #574b90;
57 | }
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mindvision-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.18.40",
11 | "@types/react": "^18.2.20",
12 | "@types/react-dom": "^18.2.7",
13 | "axios": "^1.6.2",
14 | "bootstrap": "^5.3.2",
15 | "react": "^18.2.0",
16 | "react-bootstrap": "^2.9.1",
17 | "react-dom": "^18.2.0",
18 | "react-icons": "^4.12.0",
19 | "react-router-dom": "^6.20.0",
20 | "react-scripts": "5.0.1",
21 | "typescript": "^4.9.5",
22 | "web-vitals": "^2.1.4"
23 | },
24 | "scripts": {
25 | "dev": "react-scripts start",
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": [
33 | "react-app",
34 | "react-app/jest"
35 | ]
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@types/react-router-dom": "^5.3.3"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/Appointments.css:
--------------------------------------------------------------------------------
1 | /* src/styles/Appointments.css */
2 |
3 | /* Container for the appointments page */
4 | .appointments-page {
5 | max-width: 800px;
6 | margin: 0 auto;
7 | padding: 2rem;
8 | font-family: 'Segoe UI', sans-serif;
9 | }
10 |
11 | .appointments-page h1 {
12 | text-align: center;
13 | margin-bottom: 1.5rem;
14 | color: #222;
15 | }
16 |
17 | .error {
18 | color: red;
19 | text-align: center;
20 | margin-bottom: 1rem;
21 | }
22 |
23 | /* Button to add a new appointment */
24 | .add-appointment-btn {
25 | display: block;
26 | margin: 0 auto 2rem;
27 | padding: 0.75rem 1.5rem;
28 | background-color: #6c5ce7;
29 | color: #fff;
30 | border: none;
31 | border-radius: 6px;
32 | cursor: pointer;
33 | font-weight: bold;
34 | transition: background 0.3s;
35 | }
36 |
37 | .add-appointment-btn:hover {
38 | background-color: #574b90;
39 | }
40 |
41 | /* List of appointments */
42 | .appointments-list {
43 | display: grid;
44 | gap: 1rem;
45 | }
46 |
47 | /* Individual appointment card styling */
48 | .appointment-card {
49 | background-color: #fff;
50 | border-radius: 6px;
51 | box-shadow: 0 2px 8px rgba(0,0,0,0.1);
52 | padding: 1rem;
53 | }
54 |
55 | .appointment-card h2 {
56 | margin: 0 0 0.5rem;
57 | color: #333;
58 | }
59 |
60 | .appointment-card p {
61 | margin: 0;
62 | color: #555;
63 | }
64 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | // src/routes.tsx
2 |
3 | // Importing React library
4 | import React from 'react';
5 |
6 | // Importing routing components from react-router-dom
7 | import { Routes, Route } from 'react-router-dom';
8 |
9 | // Importing individual page components
10 | import Home from './pages/Home';
11 | import Services from './pages/Services';
12 | import Blog from './pages/Blog';
13 | import Appointments from './pages/Appointments';
14 | import AddAppointment from './pages/AddAppointment';
15 | import Login from './pages/Login';
16 | import Register from './pages/Register';
17 |
18 | // Main routing component defining all application routes
19 | export default function AppRoutes() {
20 | return (
21 |
22 | {/* Route for Home page */}
23 | } />
24 |
25 | {/* Route for Services page */}
26 | } />
27 |
28 | {/* Route for Blog page */}
29 | } />
30 |
31 | {/* Route for listing Appointments */}
32 | } />
33 |
34 | {/* Route for adding a new Appointment */}
35 | } />
36 |
37 | {/* Route for Login page */}
38 | } />
39 |
40 | {/* Route for Register page */}
41 | } />
42 |
43 | );
44 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | // src/App.tsx
2 | import React from 'react';
3 | // BrowserRouter is used to enable routing using the HTML5 History API.
4 | import { BrowserRouter } from 'react-router-dom';
5 |
6 | // Import your custom routes component (renamed as AppRoutes) which defines all the application routes.
7 | import AppRoutes from './routes';
8 |
9 | // Import the Navbar component that displays the navigation bar on every page.
10 | import Navbar from './components/Navbar';
11 |
12 | // Import the UserProvider from your authentication context.
13 | // UserProvider makes authentication data (e.g., current user, login/logout functions) available to all components in the app.
14 | import { UserProvider } from './hooks/UserContext';
15 |
16 | /**
17 | * App Component
18 | * -------------
19 | * This is the root component of your application.
20 | *
21 | * It wraps the entire app with:
22 | * - BrowserRouter: Enables client-side routing.
23 | * - UserProvider: Provides global authentication state and functions.
24 | * - Navbar: Displays the navigation bar on every page.
25 | * - AppRoutes: Renders the appropriate page based on the current URL.
26 | */
27 | export default function App() {
28 | return (
29 | // BrowserRouter ensures that app can use the routing features provided by react-router-dom.
30 |
31 | {/* UserProvider makes the authentication state (user data, login/logout functions) available to all nested components */}
32 |
33 | {/* Navbar is placed outside the routes so that it appears on every page */}
34 |
35 | {/* AppRoutes contains the Route definitions and displays the appropriate page for each URL */}
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MindVision Frontend
2 |
3 | Welcome to the **MindVision** coaching platform frontend! This React application provides the user interface for registration, login, booking appointments, viewing services, and more.
4 |
5 | ## Table of Contents
6 | - [Overview](#overview)
7 | - [Key Features](#key-features)
8 | - [Tech Stack](#tech-stack)
9 | - [Getting Started](#getting-started)
10 | - [Available Scripts](#available-scripts)
11 | - [Folder Structure](#folder-structure)
12 | - [Environment Variables](#environment-variables)
13 | - [Deployment](#deployment)
14 | - [Contact](#contact)
15 |
16 | ---
17 |
18 | ## Overview
19 |
20 | **MindVision Frontend** is built with React (using [Create React App](https://create-react-app.dev/) or Vite—whichever you used), React Router for client-side routing, and Axios for API calls. Users can:
21 |
22 | - **Register** for an account or **log in**.
23 | - **Book appointments** (protected route).
24 | - **View and manage** services and blog posts.
25 | - **Explore** coaching packages.
26 |
27 | The UI is styled with custom CSS, Bootstrap
28 |
29 | ---
30 |
31 | ## Key Features
32 |
33 | 1. **Authentication**: Users can sign up, log in, and log out.
34 | 2. **Appointments**: Create and view appointments if authenticated.
35 | 3. **Services**: Display various coaching packages and benefits.
36 | 4. **Responsive Design**: Looks good on both desktop and mobile.
37 | 5. **Protected Routes**: Certain pages require a valid JWT token to access.
38 |
39 | ---
40 |
41 | ## Tech Stack
42 |
43 | - **React** (create-react-app or Vite-based)
44 | - **React Router** for routing
45 | - **Axios** for HTTP requests
46 | - **CSS** (or Sass/Bootstrap) for styling
47 |
48 | ---
49 | ## Trello and typescript were used
50 |
51 |
52 | ## Netlify
53 | https://abmindvision.netlify.app/
54 |
55 |
56 | 1. **Clone the repository**:
57 | ```bash
58 | git clone https://github.com/YourUser/MindVision_CapstoneFrontend.git
59 | cd MindVision_CapstoneFrontend
60 |
61 |
--------------------------------------------------------------------------------
/src/pages/Blog.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/Blog.tsx
2 | import React, { useEffect, useState } from 'react';
3 | import api from '../services/api'; // Axios instance for backend calls
4 | import '../styles/Blog.css';
5 |
6 | /**
7 | * Adjust the fields to match your actual data (e.g., 'title', 'content', 'author').
8 | */
9 | interface BlogPost {
10 | _id: string;
11 | title: string;
12 | content: string;
13 | author?: string;
14 | }
15 |
16 | /**
17 | * Blog Component
18 | * --------------
19 | * Fetches a list of blog posts from the backend (GET /api/blog)
20 | * and displays them in a grid of blog cards.
21 | */
22 | export default function Blog() {
23 | // State for storing the array of blog posts
24 | const [posts, setPosts] = useState([]);
25 | // State for capturing errors that occur during the fetch
26 | const [error, setError] = useState(null);
27 |
28 | /**
29 | * useEffect Hook
30 | * --------------
31 | * Fetches blog posts once when the component mounts.
32 | */
33 | useEffect(() => {
34 | const fetchPosts = async () => {
35 | try {
36 | // Adjust endpoint if your backend route is different
37 | const response = await api.get('/api/blog');
38 | console.log(response.data);
39 | setPosts(response.data);
40 | } catch (err: any) {
41 | setError('Failed to fetch blog posts.');
42 | }
43 | };
44 |
45 | fetchPosts();
46 | }, []);
47 |
48 | return (
49 |
50 |
Blog
51 |
52 | {/* If there's an error, display it */}
53 | {error &&
{error}
}
54 |
55 | {/* Display each blog post in a "blog-card" */}
56 |
57 | {posts.map((post) => (
58 |
59 |
{post.title}
60 | {post.author &&
by {post.author.name}
}
61 |
{post.content}
62 |
63 | ))}
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/Appointments.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/Appointments.tsx
2 | import React, { useEffect, useState } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 | import api from '../services/api';
5 | import '../styles/Appointments.css';
6 |
7 | interface Appointment {
8 | _id: string;
9 | date: string; // stored as an ISO string from the backend
10 | notes: string; // renamed 'description' to 'notes'
11 | }
12 |
13 | export default function Appointments() {
14 | // State to hold the fetched appointments
15 | const [appointments, setAppointments] = useState([]);
16 | // State to capture errors during the fetch
17 | const [error, setError] = useState(null);
18 |
19 | const navigate = useNavigate();
20 |
21 | // Fetch appointments when the component mounts
22 | useEffect(() => {
23 | const fetchAppointments = async () => {
24 | try {
25 | // GET /api/appointments
26 | const response = await api.get('/api/appointments');
27 | setAppointments(response.data);
28 | } catch (err: any) {
29 | setError('Failed to fetch appointments.');
30 | }
31 | };
32 | fetchAppointments();
33 | }, []);
34 |
35 | // Navigate to the "Add Appointment" page
36 | const handleAddAppointment = () => {
37 | navigate('/appointments/add');
38 | };
39 |
40 | return (
41 |
42 |
Appointments
43 | {error &&
{error}
}
44 |
45 |
46 | + Add Appointment
47 |
48 |
49 |
50 | {appointments.map((appt) => {
51 | // Convert the ISO date to a local date/time
52 | const dateObj = new Date(appt.date);
53 | const dateStr = dateObj.toLocaleDateString();
54 | const timeStr = dateObj.toLocaleTimeString([], {
55 | hour: '2-digit',
56 | minute: '2-digit'
57 | });
58 |
59 | return (
60 |
61 |
62 | {dateStr} at {timeStr}
63 |
64 |
{appt.notes}
65 |
66 | );
67 | })}
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/pages/AddAppointment.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/AddAppointment.tsx
2 | import React, { useState } from 'react';
3 | import { useNavigate } from 'react-router-dom';
4 | import api from '../services/api';
5 | import '../styles/AddAppointment.css';
6 |
7 | export default function AddAppointment() {
8 | // Local state for the appointment form
9 | const [formData, setFormData] = useState({
10 | date: '', // e.g. "2025-04-29"
11 | time: '', // e.g. "11:30"
12 | description: '', // e.g. "Consultation about goals"
13 | });
14 |
15 | // Error or success message
16 | const [message, setMessage] = useState(null);
17 | const navigate = useNavigate();
18 |
19 | // Update form data on input changes
20 | const handleChange = (
21 | e: React.ChangeEvent
22 | ) => {
23 | setFormData({ ...formData, [e.target.name]: e.target.value });
24 | };
25 |
26 | // Handle form submission
27 | const handleSubmit = async (e: React.FormEvent) => {
28 | e.preventDefault();
29 | try {
30 | // POST to /api/appointments with the form data
31 | await api.post('/api/appointments', formData);
32 | setMessage('Appointment created successfully!');
33 | // Redirect back to the appointments list after a short delay
34 | setTimeout(() => {
35 | navigate('/appointments');
36 | }, 1000);
37 | } catch (err: any) {
38 | setMessage('Failed to create appointment. Please try again.');
39 | }
40 | };
41 |
42 | return (
43 |
44 |
Add New Appointment
45 | {message &&
{message}
}
46 |
47 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/styles/Register.css:
--------------------------------------------------------------------------------
1 | /* src/styles/Register.css */
2 |
3 | /* Full-page layout with two panels: left for branding, right for the registration form */
4 | .register-page {
5 | display: flex; /* Use Flexbox to create side-by-side panels */
6 | height: 100vh; /* Full viewport height */
7 | font-family: 'Segoe UI', sans-serif; /* Page font */
8 | }
9 |
10 | /* Left panel: contains welcome content, features, and testimonial */
11 | .left-panel {
12 | flex: 1; /* Occupies half the width */
13 | background-color: #f5f5fa; /* Light background color */
14 | padding: 60px; /* Spacing around content */
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center; /* Vertically center content */
18 | color: #333; /* Dark text color */
19 | }
20 |
21 | /* Feature list styling in the left panel */
22 | .feature-list {
23 | margin: 20px 0; /* Vertical spacing */
24 | line-height: 1.6; /* Increased line spacing for readability */
25 | }
26 |
27 | /* Testimonial styling */
28 | .testimonial {
29 | margin-top: 30px; /* Space above testimonial */
30 | font-style: italic; /* Italic text style */
31 | color: #666; /* Medium gray color */
32 | }
33 |
34 | /* Right panel: contains the registration form */
35 | .right-panel {
36 | flex: 1; /* Occupies the other half of the width */
37 | display: flex;
38 | justify-content: center; /* Center the form horizontally */
39 | align-items: center; /* Center the form vertically */
40 | padding: 60px;
41 | background-color: #ffffff; /* White background for contrast */
42 | }
43 |
44 | /* Form container styling */
45 | .register-form {
46 | width: 100%;
47 | max-width: 400px; /* Limit maximum form width */
48 | display: flex;
49 | flex-direction: column; /* Stack form elements vertically */
50 | }
51 |
52 | /* Form title styling */
53 | .register-form h2 {
54 | margin-bottom: 20px; /* Space below the title */
55 | color: #222; /* Dark text for the title */
56 | }
57 |
58 | /* Input fields and select dropdown styling */
59 | .register-form input,
60 | .register-form select {
61 | margin-bottom: 15px; /* Space between form elements */
62 | padding: 12px; /* Padding inside each input */
63 | border: 1px solid #ccc; /* Light gray border */
64 | border-radius: 6px; /* Rounded corners */
65 | font-size: 16px; /* Set font size */
66 | }
67 |
68 | /* Submit button styling */
69 | .register-form button {
70 | background-color: #6a47d4; /* Purple background */
71 | color: white; /* White text */
72 | border: none; /* No border */
73 | padding: 12px; /* Padding for the button */
74 | font-size: 16px; /* Font size */
75 | border-radius: 6px; /* Rounded corners */
76 | cursor: pointer; /* Cursor changes to pointer on hover */
77 | transition: background-color 0.2s ease; /* Smooth background transition */
78 | }
79 |
80 | /* Button hover effect */
81 | .register-form button:hover {
82 | background-color: #593bb6; /* Darker purple on hover */
83 | }
84 |
85 | /* Message styling for success or error feedback */
86 | .message {
87 | margin-top: 10px;
88 | color: green; /* Green text for success messages */
89 | }
90 |
91 | /* Link styling below the form */
92 | .link {
93 | margin-top: 16px;
94 | text-align: center; /* Center the link text */
95 | font-size: 14px; /* Smaller font size */
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | // src/components/Navbar.tsx
2 |
3 | import React, { useContext, useState } from 'react';
4 | import { Link, useNavigate } from 'react-router-dom';
5 | import { UserContext } from '../hooks/UserContext';
6 | import '../styles/Navbar.css';
7 |
8 | export default function Navbar() {
9 | // Get current user object and logout function from UserContext
10 | const { user, logout } = useContext(UserContext);
11 |
12 | // Hook to navigate programmatically (e.g., redirect after logout)
13 | const navigate = useNavigate();
14 |
15 | // Track whether mobile menu is open or closed
16 | const [isMenuOpen, setIsMenuOpen] = useState(false);
17 |
18 | // Handle user logout: clear auth state and go to homepage
19 | const handleLogout = () => {
20 | logout(); // Clear user state and remove token
21 | navigate('/'); // Redirect to home
22 | };
23 |
24 | // Toggle mobile menu open/closed
25 | const toggleMenu = () => {
26 | setIsMenuOpen(!isMenuOpen);
27 | };
28 |
29 | // Close mobile menu when any link is clicked
30 | const closeMenu = () => {
31 | if (isMenuOpen) setIsMenuOpen(false);
32 | };
33 |
34 | return (
35 |
36 |
37 | {/* Logo / brand link - always shows */}
38 |
39 | MindVision
40 |
41 |
42 | {/* Hamburger icon for mobile menu toggle */}
43 |
44 |
45 |
46 |
47 |
48 |
49 | {/* Navigation links - adapt to screen size */}
50 |
51 | {/* Always visible links */}
52 |
53 | Home
54 |
55 |
56 | Services
57 |
58 |
59 | Blog
60 |
61 |
62 | {/* Conditional links based on auth status */}
63 | {user ? (
64 | <>
65 | {/* Only visible when logged in */}
66 |
67 | Appointments
68 |
69 |
70 | Add Appointment
71 |
72 |
73 | {
75 | closeMenu();
76 | handleLogout();
77 | }}
78 | className="logout-button"
79 | >
80 | Logout
81 |
82 |
83 | >
84 | ) : (
85 | <>
86 | {/* Only visible when logged out */}
87 |
88 | Login
89 |
90 |
91 | Register
92 |
93 | >
94 | )}
95 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/styles/Navbar.css:
--------------------------------------------------------------------------------
1 | /* src/styles/Navbar.css */
2 |
3 | /* Main navbar styles */
4 | .navbar {
5 | background-color: white;
6 | height: 60px;
7 | width: 100%;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | position: sticky;
12 | top: 0;
13 | z-index: 999;
14 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
15 | }
16 |
17 | .navbar-container {
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | width: 100%;
22 | max-width: 1200px;
23 | padding: 0 20px;
24 | height: 60px;
25 | }
26 |
27 | /* Brand/logo styles */
28 | .navbar-brand {
29 | margin-right: 20px;
30 | }
31 |
32 | .navbar-brand a {
33 | color: #3a6ea5;
34 | font-size: 1.5rem;
35 | font-weight: 700;
36 | text-decoration: none;
37 | }
38 |
39 | /* Navigation links */
40 | .navbar-links {
41 | display: flex;
42 | flex-direction: row;
43 | align-items: center;
44 | list-style: none;
45 | padding: 0;
46 | margin: 0;
47 | }
48 |
49 | .navbar-links li {
50 | margin-left: 30px;
51 | display: inline-block;
52 | }
53 |
54 | .navbar-links a {
55 | color: #343a40;
56 | text-decoration: none;
57 | font-weight: 500;
58 | padding: 0.5rem 0;
59 | transition: color 0.3s;
60 | }
61 |
62 | .navbar-links a:hover {
63 | color: #3a6ea5;
64 | }
65 |
66 | /* Logout button */
67 | .logout-button {
68 | background-color: #ff6b6b;
69 | color: white;
70 | border: none;
71 | padding: 0.5rem 1rem;
72 | border-radius: 4px;
73 | cursor: pointer;
74 | font-weight: 600;
75 | transition: background-color 0.3s;
76 | }
77 |
78 | .logout-button:hover {
79 | background-color: #ff5252;
80 | }
81 |
82 | /* Mobile menu icon - hidden by default */
83 | .menu-icon {
84 | display: none;
85 | }
86 |
87 | .menu-icon-bar {
88 | display: block;
89 | width: 25px;
90 | height: 3px;
91 | margin: 5px auto;
92 | background-color: #343a40;
93 | transition: all 0.3s ease-in-out;
94 | }
95 |
96 | /* Mobile responsive styles */
97 | @media screen and (max-width: 768px) {
98 | .navbar-container {
99 | justify-content: space-between;
100 | }
101 |
102 | .menu-icon {
103 | display: block;
104 | cursor: pointer;
105 | z-index: 1000;
106 | }
107 |
108 | .menu-icon-bar.open:nth-child(1) {
109 | transform: rotate(-45deg) translate(-5px, 6px);
110 | }
111 |
112 | .menu-icon-bar.open:nth-child(2) {
113 | opacity: 0;
114 | }
115 |
116 | .menu-icon-bar.open:nth-child(3) {
117 | transform: rotate(45deg) translate(-5px, -6px);
118 | }
119 |
120 | .navbar-links {
121 | display: flex;
122 | flex-direction: column;
123 | width: 100%;
124 | height: auto;
125 | position: absolute;
126 | top: 60px;
127 | left: -100%;
128 | opacity: 0;
129 | transition: all 0.5s ease;
130 | background-color: white;
131 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
132 | padding: 1rem 0;
133 | }
134 |
135 | .navbar-links.active {
136 | left: 0;
137 | opacity: 1;
138 | transition: all 0.5s ease;
139 | z-index: 1;
140 | }
141 |
142 | .navbar-links li {
143 | margin: 1rem 0;
144 | text-align: center;
145 | width: 100%;
146 | }
147 |
148 | .navbar-links a {
149 | display: block;
150 | width: 100%;
151 | padding: 0.5rem 0;
152 | }
153 | }
--------------------------------------------------------------------------------
/src/hooks/UserContext.tsx:
--------------------------------------------------------------------------------
1 | // src/hooks/UserContext.tsx
2 | import React, { createContext, useState, ReactNode, useEffect } from 'react';
3 | import api from '../services/api';
4 |
5 | // Define the structure for our user data
6 | interface User {
7 | _id: string;
8 | name: string;
9 | email: string;
10 | token: string;
11 | }
12 |
13 | // Define the context interface including login, register, logout, and update functions.
14 | interface UserContextType {
15 | user: User | null;
16 | login: (email: string, password: string) => Promise;
17 | register: (name: string, email: string, password: string) => Promise;
18 | logout: () => void;
19 | updateUser: (userData: Partial) => void;
20 | }
21 |
22 | // Create the context with default (dummy) implementations.
23 | export const UserContext = createContext({
24 | user: null,
25 | login: async () => {},
26 | register: async () => {},
27 | logout: () => {},
28 | updateUser: () => {}
29 | });
30 |
31 | // Provider props type.
32 | interface Props {
33 | children: ReactNode;
34 | }
35 |
36 | // The UserProvider wraps the app and provides authentication state.
37 | export const UserProvider = ({ children }: Props) => {
38 | // Initialize user state from localStorage for persistence.
39 | const [user, setUser] = useState(() => {
40 | const storedUser = localStorage.getItem('user');
41 | return storedUser ? JSON.parse(storedUser) : null;
42 | });
43 |
44 | // Update localStorage and the Axios default header whenever user state changes.
45 | useEffect(() => {
46 | if (user) {
47 | localStorage.setItem('user', JSON.stringify(user));
48 | api.defaults.headers.common['Authorization'] = `Bearer ${user.token}`;
49 | } else {
50 | localStorage.removeItem('user');
51 | delete api.defaults.headers.common['Authorization'];
52 | }
53 | }, [user]);
54 |
55 | // Login function: calls POST /api/auth/login then GET /api/auth/me to get user info.
56 | const login = async (email: string, password: string) => {
57 | try {
58 | const response = await api.post('/api/auth/login', { email, password });
59 | const token = response.data.token;
60 | // Set the token in the default header
61 | api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
62 | // Now fetch the user info using GET /api/auth/me
63 | const meResponse = await api.get('/api/auth/me');
64 | const userData = { ...meResponse.data, token };
65 | setUser(userData);
66 | } catch (err: any) {
67 | throw new Error(err.response?.data?.msg || 'Login failed');
68 | }
69 | };
70 |
71 | // Register function: calls POST /api/auth/register then GET /api/auth/me.
72 | const register = async (name: string, email: string, password: string) => {
73 | try {
74 | const response = await api.post('/api/auth/register', { name, email, password });
75 | const token = response.data.token;
76 | // Set the token in the default header
77 | api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
78 | // Now fetch the user info using GET /api/auth/me
79 | const meResponse = await api.get('/api/auth/me');
80 | const userData = { ...meResponse.data, token };
81 | setUser(userData);
82 | } catch (err: any) {
83 | throw new Error(err.response?.data?.msg || 'Registration failed');
84 | }
85 | };
86 |
87 | // Logout function: clears the user state.
88 | const logout = () => {
89 | setUser(null);
90 | };
91 |
92 | // updateUser function: merges new user data with the existing state.
93 | const updateUser = (userData: Partial) => {
94 | setUser(prev => (prev ? { ...prev, ...userData } : null));
95 | };
96 |
97 | return (
98 |
99 | {children}
100 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/styles/Login.css:
--------------------------------------------------------------------------------
1 | /* src/styles/Login.css */
2 |
3 | /* Full-page layout for the login page */
4 | .login-page {
5 | display: flex; /* Create two side-by-side panels */
6 | min-height: 100vh; /* Full viewport height */
7 | background-color: #f9f9ff; /* Light background color */
8 | }
9 |
10 | /* LEFT PANEL: Branding / Welcome Content */
11 | .login-left {
12 | flex: 1; /* Take up half the width */
13 | padding: 4rem 3rem; /* Spacing inside the panel */
14 | background-color: #f4f3ff; /* Light purple background for contrast */
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center; /* Center content vertically */
18 | align-items: center; /* Center content horizontally */
19 | text-align: left;
20 | max-width: 480px;
21 | margin: 0 auto;
22 | }
23 |
24 | /* RIGHT PANEL: Login Form Container */
25 | .login-right {
26 | flex: 1; /* Take up the remaining half */
27 | padding: 4rem 3rem; /* Spacing inside the panel */
28 | background-color: #f4f3ff; /* Consistent background with left panel */
29 | display: flex;
30 | flex-direction: column;
31 | justify-content: center;
32 | }
33 |
34 | /* Login box styling */
35 | .login-container {
36 | width: 100%;
37 | max-width: 380px; /* Limit maximum width */
38 | background-color: #fff; /* White background for the form */
39 | padding: 2rem; /* Spacing inside the form */
40 | border-radius: 12px; /* Rounded corners */
41 | box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); /* Subtle shadow */
42 | }
43 |
44 | /* Form title styling */
45 | .login-title {
46 | font-size: 1.5rem;
47 | font-weight: 600;
48 | margin-bottom: 1.5rem;
49 | color: #222;
50 | }
51 |
52 | /* Form layout */
53 | .login-form {
54 | display: flex;
55 | flex-direction: column;
56 | gap: 1rem; /* Space between input fields */
57 | }
58 |
59 | /* Input fields styling */
60 | .login-input {
61 | padding: 0.75rem 1rem;
62 | font-size: 1rem;
63 | border: 1px solid #ccc;
64 | border-radius: 6px;
65 | }
66 |
67 | /* Submit button styling */
68 | .login-button {
69 | padding: 0.75rem;
70 | background-color: #6c5ce7; /* Purple background */
71 | color: white;
72 | font-weight: bold;
73 | border: none;
74 | border-radius: 6px;
75 | cursor: pointer;
76 | transition: background 0.3s; /* Smooth background transition */
77 | }
78 |
79 | .login-button:hover {
80 | background-color: #574b90; /* Darker purple on hover */
81 | }
82 |
83 | /* Error message styling */
84 | .login-error {
85 | color: red;
86 | font-size: 0.9rem;
87 | margin-top: 0.5rem;
88 | }
89 |
90 | /* Signup link styling */
91 | .signup-text {
92 | margin-top: 1rem;
93 | text-align: center;
94 | font-size: 0.9rem;
95 | }
96 |
97 | .signup-text a {
98 | color: #6c5ce7;
99 | text-decoration: none;
100 | }
101 |
102 | /* Additional branding styles for the left panel */
103 | .welcome-title {
104 | font-size: 2.5rem;
105 | font-weight: 800;
106 | line-height: 1.2;
107 | color: #222;
108 | margin-bottom: 1rem;
109 | }
110 |
111 | .welcome-subtext {
112 | font-size: 1rem;
113 | color: #555;
114 | margin-bottom: 2rem;
115 | }
116 |
117 | .benefits-list {
118 | list-style: none;
119 | padding: 0;
120 | margin-bottom: 2rem;
121 | color: #333;
122 | }
123 |
124 | .benefits-list li {
125 | margin-bottom: 0.5rem;
126 | font-size: 1rem;
127 | }
128 |
129 | .testimonial {
130 | font-style: italic;
131 | color: #555;
132 | }
133 |
134 | .client-credit {
135 | margin-top: 0.5rem;
136 | font-style: normal;
137 | font-size: 0.9rem;
138 | color: #777;
139 | }
140 |
141 | /* Animated halo for branding (if used) */
142 | .halo {
143 | display: inline-block;
144 | animation: float 2s ease-in-out infinite;
145 | }
146 |
147 | @keyframes float {
148 | 0%, 100% { transform: translateY(0); }
149 | 50% { transform: translateY(-4px); }
150 | }
151 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/Login.tsx
2 | import React, { useState, useContext } from 'react';
3 | import { Link, useNavigate } from 'react-router-dom';
4 | import { UserContext } from '../hooks/UserContext';
5 | import '../styles/Login.css';
6 |
7 | /**
8 | * Login Component
9 | * ----------------
10 | * This component renders a two-panel layout:
11 | * - Left Panel: Branding and welcome message.
12 | * - Right Panel: Login form.
13 | *
14 | * It uses the global UserContext to call the login function. Upon successful login,
15 | * it redirects the user to the dashboard (or home). Errors are displayed if login fails.
16 | */
17 | const Login: React.FC = () => {
18 | // Retrieve the login function from UserContext
19 | const { login } = useContext(UserContext);
20 | // useNavigate allows for programmatic redirection
21 | const navigate = useNavigate();
22 |
23 | // Local state for email and password input fields
24 | const [email, setEmail] = useState('');
25 | const [password, setPassword] = useState('');
26 | // Local state to hold error messages if login fails
27 | const [error, setError] = useState(null);
28 |
29 | // Handle form submission
30 | const handleSubmit = async (e: React.FormEvent) => {
31 | e.preventDefault(); // Prevent page reload on form submit
32 | if (!email || !password) {
33 | setError('Email and password are required.');
34 | return;
35 | }
36 | setError(null);
37 | try {
38 | // Call the login function from context
39 | await login(email, password);
40 | // On successful login, redirect to the dashboard (or home page)
41 | navigate('/dashboard');
42 | } catch (err: any) {
43 | // Capture and display the error message if login fails
44 | setError(err.message);
45 | console.error("Login error:", err);
46 | }
47 | };
48 |
49 | return (
50 |
51 | {/* LEFT PANEL: Branding and Welcome Information */}
52 |
53 |
54 | Welcome to MindVision 🪐
55 |
56 |
57 | Your journey to clarity and confidence starts here.
58 |
59 |
60 | ✓ Personal Growth
61 | ✓ 1-on-1 Sessions
62 | ✓ Secure & Confidential
63 |
64 |
65 | "Working with MindVision changed my life!"
66 |
67 | - A grateful client 💜
68 |
69 |
70 |
71 | {/* RIGHT PANEL: Login Form */}
72 |
73 |
74 |
Login to MindVision
75 |
76 | {/* Email Input */}
77 | setEmail(e.target.value)}
83 | required
84 | />
85 | {/* Password Input */}
86 | setPassword(e.target.value)}
92 | required
93 | />
94 | {/* Display error if login fails */}
95 | {error && {error}
}
96 | {/* Submit Button */}
97 |
98 | Login
99 |
100 |
101 | {/* Link to the Register page */}
102 |
103 | Don't have an account? Sign up
104 |
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default Login;
112 |
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import '../styles/Home.css';
4 |
5 | // Shape of quote from API
6 | interface Quote {
7 | content: string;
8 | author: string;
9 | }
10 |
11 | export default function Home() {
12 | const navigate = useNavigate();
13 | const [quote, setQuote] = useState(null);
14 | const [error, setError] = useState(null);
15 |
16 | // Fetch quote from quotable API (using HTTPS to avoid mixed-content issues)
17 | const fetchQuote = async () => {
18 | try {
19 | // This endpoint returns an array with one quote object
20 | const response = await fetch('https://api.quotable.io/quotes/random');
21 | if (!response.ok) throw new Error('Failed to fetch quote');
22 | const data = await response.json();
23 | // data is an array of length 1, so we use data[0]
24 | setQuote(data[0]);
25 | } catch (err: any) {
26 | setError(err.message);
27 | }
28 | };
29 |
30 | // Load quote on mount
31 | useEffect(() => {
32 | fetchQuote();
33 | }, []);
34 |
35 | return (
36 |
37 | {/* 👋 Hero Section */}
38 |
39 |
40 |
41 |
Welcome to MindVision Coaching
42 |
Empower yourself with inspiration!
43 |
navigate('/login')}>
44 | Book Your Free Consultation
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {/* 📘 About Section */}
56 |
57 |
About My Coaching
58 |
59 |
60 |
61 | I help clients unlock their full potential through personalized
62 | coaching strategies that address mindset, goals, and personal
63 | growth. My approach combines proven psychological techniques with
64 | practical strategies for real-world results.
65 |
66 |
67 | Every coaching journey is tailored to your unique needs and
68 | aspirations, creating a supportive space where transformation
69 | happens naturally.
70 |
71 |
72 |
73 |
74 | {/* 💎 Coaching Approach Section (NEW) */}
75 |
76 |
My Coaching Approach
77 |
78 |
79 |
🧠
80 |
Mindset Transformation
81 |
82 | Identify and reframe limiting beliefs that hold you back from
83 | reaching your full potential.
84 |
85 |
86 |
87 |
🎯
88 |
Strategic Goal Setting
89 |
90 | Create meaningful goals with clear action steps aligned with your
91 | values and aspirations.
92 |
93 |
94 |
95 |
⚡
96 |
Sustainable Growth
97 |
98 | Develop habits and systems that support long-term success and
99 | continued personal evolution.
100 |
101 |
102 |
103 |
104 |
105 |
106 | {/* 💬 Quote Section */}
107 |
108 | {error &&
{error}
}
109 | {quote ? (
110 |
111 | "{quote.content}"
112 |
113 |
114 | ) : (
115 |
Loading quote...
116 | )}
117 |
118 | New Quote
119 |
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/src/pages/Register.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/Register.tsx
2 | import React, { useState, useContext } from 'react';
3 | import { Link, useNavigate } from 'react-router-dom';
4 | import '../styles/Register.css';
5 | import { UserContext } from '../hooks/UserContext';
6 |
7 | /**
8 | * Register Component
9 | * ------------------
10 | * This component renders the registration form with a two-panel layout.
11 | * The left panel contains branding and feature information, while the right panel
12 | * contains the form where a user can register.
13 | *
14 | * When the form is submitted, it calls the register function from UserContext.
15 | * Upon successful registration, it fetches the full user info via GET /api/auth/me,
16 | * stores the data (including the JWT token) in the context, and redirects the user
17 | * to the dashboard.
18 | */
19 | const Register: React.FC = () => {
20 | // Local state to manage form data
21 | const [formData, setFormData] = useState({
22 | name: '',
23 | email: '',
24 | password: '',
25 | role: 'client',
26 | });
27 |
28 | // Local state for displaying success/error messages to the user
29 | const [message, setMessage] = useState('');
30 |
31 | // Get the register function from the global UserContext
32 | const { register } = useContext(UserContext);
33 | // useNavigate hook to programmatically navigate to a different route after registration
34 | const navigate = useNavigate();
35 |
36 | // Handle input changes by updating the formData state
37 | const handleChange = (
38 | e: React.ChangeEvent
39 | ) => {
40 | setFormData({ ...formData, [e.target.name]: e.target.value });
41 | };
42 |
43 | // Handle form submission
44 | const handleSubmit = async (e: React.FormEvent) => {
45 | e.preventDefault();
46 | try {
47 | // Call the register function from context, which performs the registration
48 | // and immediately fetches the user info via GET /api/auth/me.
49 | await register(formData.name, formData.email, formData.password);
50 | // If registration is successful, update the message state
51 | setMessage('✅ Registration successful!');
52 | // Redirect the user to the dashboard (or home) page
53 | navigate('/dashboard');
54 | } catch (err: any) {
55 | console.error('❌ Registration failed:', err.response?.data || err.message);
56 | // Display the error message from the backend, if available
57 | setMessage(err.response?.data?.msg || '❌ Registration failed. Please try again.');
58 | }
59 | };
60 |
61 | return (
62 |
63 | {/* Left Panel: Branding and Features */}
64 |
65 |
Welcome to MindVision 💫
66 |
Your journey to clarity and confidence starts here.
67 |
68 |
✔ Personal Growth
69 |
✔ 1-on-1 Sessions
70 |
✔ Secure & Confidential
71 |
72 |
73 |
"Working with MindVision changed my life!"
74 |
- A grateful client 💜
75 |
76 |
77 |
78 | {/* Right Panel: Registration Form */}
79 |
125 |
126 | );
127 | };
128 |
129 | export default Register;
130 |
--------------------------------------------------------------------------------
/src/pages/Services.tsx:
--------------------------------------------------------------------------------
1 | // src/pages/Services.tsx
2 |
3 | import React from 'react';
4 | import { Link } from 'react-router-dom';
5 | import '../styles/Services.css';
6 |
7 | export default function Services() {
8 | return (
9 |
10 | {/* Header */}
11 |
12 |
Services
13 |
14 | Personalized coaching solutions to transform your mindset and achieve your goals
15 |
16 |
17 |
18 | {/* Pricing Cards */}
19 |
20 |
Coaching Programs
21 |
22 |
23 |
24 |
Discovery Session
25 |
$99
26 |
One-time session
27 |
28 |
29 |
30 | 90-minute deep dive consultation
31 | Personalized action plan
32 | Identify key limiting beliefs
33 | Goal setting framework
34 |
35 |
36 |
Get Started
37 |
38 |
39 |
40 |
Most Popular
41 |
42 |
Transformation Package
43 |
$497
44 |
Monthly
45 |
46 |
47 |
48 | 4 one-hour coaching sessions per month
49 | Weekly accountability check-ins
50 | Personalized growth exercises
51 | Email support between sessions
52 | Custom resource library access
53 |
54 |
55 |
Transform Your Life
56 |
57 |
58 |
59 |
60 |
VIP Intensive
61 |
$1,997
62 |
3-month commitment
63 |
64 |
65 |
66 | 6 one-hour coaching sessions per month
67 | Unlimited text/email support
68 | Customized mindset training
69 | Personal development assessment
70 | VIP day (4 hours of focused coaching)
71 | Priority scheduling
72 |
73 |
74 |
Apply Now
75 |
76 |
77 |
78 |
79 |
Executive Mastery
80 |
$2,997
81 |
3-month commitment
82 |
83 |
84 |
85 | 8 one-hour sessions per month
86 | Leadership development focus
87 | Strategic career planning
88 | Work-life balance optimization
89 | Communication skills enhancement
90 | Team dynamics coaching
91 | 24/7 priority support
92 |
93 |
94 |
Elevate Your Leadership
95 |
96 |
97 |
98 |
99 | {/* Benefits Section */}
100 |
101 |
How You'll Benefit
102 |
103 |
104 |
✨
105 |
Clarity & Direction
106 |
Gain crystal-clear vision on your goals and the optimal path to achieve them
107 |
108 |
109 |
💪
110 |
Confidence & Resilience
111 |
Develop unwavering self-belief and the ability to overcome any obstacle
112 |
113 |
114 |
🚀
115 |
Accelerated Results
116 |
Achieve your goals faster with proven strategies and personalized guidance
117 |
118 |
119 |
🔄
120 |
Lasting Transformation
121 |
Create sustainable change through new mindsets and empowering habits
122 |
123 |
124 |
125 |
126 | {/* CTA Section */}
127 |
128 |
129 |
Ready to Transform Your Life?
130 |
131 | Take the first step toward becoming your best self today.
132 |
133 |
Begin Your Journey Now
134 |
135 |
136 |
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/src/styles/Services.css:
--------------------------------------------------------------------------------
1 | .services-container {
2 | /* Instead of padding: 5rem 5%, we use a fixed max-width + auto margin
3 | to avoid horizontal overflow on wide screens. */
4 | background-color: var(--light);
5 | max-width: 1400px; /* The container won’t exceed 1400px in width */
6 | margin: 0 auto; /* Centers the container horizontally */
7 | padding: 5rem 2rem; /* Vertical and horizontal padding */
8 | box-sizing: border-box; /* Ensures padding is included in total width */
9 | }
10 |
11 | /* =============== SECTION HEADINGS =============== */
12 | .services-header,
13 | .pricing-section,
14 | .benefits-section,
15 | .cta-section {
16 | text-align: center;
17 | margin-bottom: 4rem;
18 | }
19 |
20 | .services-header h1,
21 | .pricing-section h2,
22 | .benefits-section h2,
23 | .cta-section h2 {
24 | font-family: 'Playfair Display', serif;
25 | font-size: 2.5rem;
26 | color: var(--secondary);
27 | margin-bottom: 1rem;
28 | }
29 |
30 | .services-tagline,
31 | .cta-section p {
32 | font-size: 1.2rem;
33 | max-width: 700px;
34 | margin: 0 auto;
35 | color: var(--primary);
36 | }
37 |
38 | /* =============== PRICING SECTION =============== */
39 | .pricing-cards {
40 | display: grid;
41 | grid-template-columns: repeat(4, 1fr);
42 | gap: 2rem;
43 | max-width: 1200px; /* The grid won’t exceed 1200px */
44 | margin: 0 auto 5rem; /* Centered and spaced from bottom */
45 | padding: 0 2rem; /* Horizontal padding so cards aren’t flush against screen edges */
46 | box-sizing: border-box;
47 | }
48 |
49 | .pricing-card {
50 | background-color: white;
51 | border-radius: 16px;
52 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.07);
53 | padding: 2rem;
54 | position: relative;
55 | transition: transform 0.3s, box-shadow 0.3s;
56 | height: 100%;
57 | display: flex;
58 | flex-direction: column;
59 | }
60 |
61 | .pricing-card:hover {
62 | transform: translateY(-5px);
63 | box-shadow: 0 15px 30px rgba(0, 0, 0, 0.12);
64 | }
65 |
66 | .popular-badge {
67 | position: absolute;
68 | top: -12px;
69 | right: -12px;
70 | background-color: var(--accent);
71 | color: white;
72 | padding: 0.4rem 0.8rem;
73 | font-size: 0.75rem;
74 | font-weight: bold;
75 | border-radius: 6px;
76 | }
77 |
78 | .pricing-card-header h3 {
79 | color: var(--secondary);
80 | font-size: 1.4rem;
81 | margin-bottom: 0.5rem;
82 | }
83 |
84 | .pricing-amount {
85 | font-size: 2rem;
86 | font-weight: bold;
87 | color: var(--primary);
88 | }
89 |
90 | .pricing-period {
91 | color: var(--text);
92 | font-size: 0.95rem;
93 | margin-bottom: 1rem;
94 | }
95 |
96 | .pricing-features ul {
97 | list-style: none;
98 | padding: 0;
99 | margin: 1rem 0;
100 | font-size: 0.95rem;
101 | }
102 |
103 | .pricing-features li {
104 | margin-bottom: 0.5rem;
105 | }
106 |
107 | .pricing-cta {
108 | display: inline-block;
109 | margin-top: 1rem;
110 | background-color: var(--secondary);
111 | color: white;
112 | padding: 0.6rem 1.2rem;
113 | font-weight: 600;
114 | border-radius: 4px;
115 | text-decoration: none;
116 | transition: background-color 0.3s;
117 | }
118 |
119 | .pricing-cta:hover {
120 | background-color: var(--primary);
121 | }
122 |
123 | /* =============== BENEFITS SECTION =============== */
124 | .benefits-container {
125 | display: grid;
126 | grid-template-columns: repeat(4, 1fr);
127 | gap: 2rem;
128 | max-width: 2400px; /* The grid won’t exceed 1200px */
129 | margin: 0 auto 5rem; /* Centered and spaced from bottom */
130 | padding: 0 2rem; /* Horizontal padding so boxes aren’t flush against screen edges */
131 | box-sizing: border-box;
132 | }
133 |
134 | .benefit-item {
135 | background-color: white;
136 | padding: 2rem;
137 | border-radius: 12px;
138 | box-shadow: 0 8px 18px rgba(0, 0, 0, 0.07);
139 | text-align: center;
140 | height: 100%;
141 | display: flex;
142 | flex-direction: column;
143 | }
144 |
145 | .benefit-icon {
146 | font-size: 2.5rem;
147 | margin-bottom: 1rem;
148 | color: var(--accent);
149 | }
150 |
151 | .benefit-item h3 {
152 | color: var(--secondary);
153 | font-size: 1.25rem;
154 | margin-bottom: 0.5rem;
155 | }
156 |
157 | .benefit-item p {
158 | font-size: 0.95rem;
159 | color: var(--text);
160 | }
161 |
162 | /* =============== CTA SECTION =============== */
163 | .cta-section {
164 | padding: 5rem 2rem;
165 | background-color: #ffffff;
166 | box-shadow: 0 15px 40px rgba(0, 0, 0, 0.05);
167 | margin: 6rem auto 0;
168 | max-width: 900px;
169 | border-radius: 12px;
170 | }
171 |
172 | .cta-content {
173 | text-align: center;
174 | }
175 |
176 | .cta-content h2 {
177 | font-family: 'Playfair Display', serif;
178 | font-size: 2.8rem;
179 | font-weight: 700;
180 | color: var(--secondary);
181 | margin-bottom: 1rem;
182 | }
183 |
184 | .cta-content p {
185 | font-size: 1.15rem;
186 | color: var(--text);
187 | margin-bottom: 2.2rem;
188 | max-width: 600px;
189 | margin-inline: auto;
190 | line-height: 1.6;
191 | }
192 |
193 | .cta-content .highlight {
194 | font-weight: 600;
195 | color: var(--primary);
196 | }
197 |
198 | .cta-button {
199 | display: inline-block;
200 | background-color: var(--primary);
201 | color: white;
202 | font-size: 1rem;
203 | padding: 0.9rem 2.2rem;
204 | border-radius: 8px;
205 | text-decoration: none;
206 | font-weight: 600;
207 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
208 | transition: background-color 0.3s ease, transform 0.2s ease;
209 | }
210 |
211 | .cta-button:hover {
212 | background-color: var(--secondary);
213 | transform: translateY(-2px);
214 | }
215 |
216 | /* =============== RESPONSIVE BREAKPOINTS =============== */
217 | @media (max-width: 1200px) {
218 | .pricing-cards,
219 | .benefits-container {
220 | grid-template-columns: repeat(2, 1fr);
221 | }
222 | }
223 |
224 | @media (max-width: 768px) {
225 | .pricing-cards,
226 | .benefits-container {
227 | grid-template-columns: 1fr;
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/styles/Home.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 1100px) {
2 | /* Responsive styling for medium screens */
3 | .benefits-container {
4 | justify-content: center;
5 | }
6 |
7 | .benefit-item {
8 | flex-basis: calc(50% - 2rem);
9 | margin: 1rem auto;
10 | }
11 | }
12 |
13 | @media (max-width: 768px) {
14 | /* Responsive styling for small screens */
15 | .hero-content {
16 | flex-direction: column;
17 | }
18 |
19 | .hero-text {
20 | padding-right: 0;
21 | margin-bottom: 2rem;
22 | }
23 |
24 | .benefit-item {
25 | flex-basis: 100%;
26 | max-width: 400px;
27 | margin: 1rem auto;
28 | }
29 | }/* Import Google Fonts - Montserrat for body text and Playfair Display for headings */
30 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&family=Playfair+Display:wght@400;700&display=swap');
31 |
32 | /* CSS Variables for consistent color scheme throughout the site */
33 | :root {
34 | --primary: #3a6ea5;
35 | --secondary: #004e98;
36 | --accent: #ff6b6b;
37 | --light: #f8f9fa;
38 | --dark: #343a40;
39 | --text: #212529; /* Main text color */
40 | }
41 |
42 | /* Reset default browser styles */
43 | * {
44 | margin: 0;
45 | padding: 0;
46 | box-sizing: border-box;
47 | }
48 |
49 | /* Base body styles */
50 | body {
51 | font-family: 'Montserrat', sans-serif;
52 | line-height: 1.6;
53 | color: var(--text);
54 | background-color: var(--light);
55 | }
56 |
57 | /* Main container that wraps all content */
58 | .home-container {
59 | width: 100%;
60 | }
61 |
62 | /* Hero section - full height landing area with gradient background */
63 | .hero-section {
64 | min-height: 100vh;
65 | padding: 80px 5% 40px;
66 | display: flex;
67 | align-items: center;
68 | background: linear-gradient(135deg, rgba(0,78,152,0.05) 0%, rgba(58,110,165,0.1) 100%);
69 | }
70 |
71 | /* Content container for the hero section - uses flexbox for responsive layout */
72 | .hero-content {
73 | display: flex;
74 | width: 100%;
75 | max-width: 1200px;
76 | margin: 0 auto;
77 | align-items: center;
78 | justify-content: space-between;
79 | }
80 |
81 | /* Left side of hero with heading and call-to-action */
82 | .hero-text {
83 | flex: 1;
84 | padding-right: 2rem;
85 | }
86 |
87 | /* Main headline styling with elegant serif font */
88 | .hero-text h1 {
89 | font-family: 'Playfair Display', serif;
90 | font-size: 3rem;
91 | margin-bottom: 1rem;
92 | color: var(--secondary);
93 | }
94 |
95 | /* Subtitle text below main heading */
96 | .hero-subtitle {
97 | font-size: 1.2rem;
98 | margin-bottom: 2rem;
99 | max-width: 600px;
100 | }
101 |
102 | /* Call-to-action button styling */
103 | .cta-button {
104 | background-color: var(--primary);
105 | color: white;
106 | border: none;
107 | padding: 0.8rem 2rem;
108 | border-radius: 4px;
109 | font-weight: 600;
110 | font-size: 1rem;
111 | cursor: pointer;
112 | transition: background-color 0.3s; /* Smooth transition for hover effect */
113 | }
114 |
115 | /* Button hover state - darkens slightly */
116 | .cta-button:hover {
117 | background-color: var(--secondary);
118 | }
119 |
120 | /* Right side of hero containing the profile image */
121 | .hero-image {
122 | flex: 1;
123 | display: flex;
124 | justify-content: center;
125 | }
126 |
127 | /* Circular container for profile image */
128 | .profile-circle {
129 | width: 350px;
130 | height: 350px;
131 | border-radius: 50%;
132 | overflow: hidden;
133 | border: 8px solid white; /* White border around circle */
134 | box-shadow: 0 10px 20px rgba(0,0,0,0.1); /* Subtle shadow for depth */
135 | background-color: #e9ecef; /* Fallback color if image fails to load */
136 | display: flex;
137 | align-items: center;
138 | justify-content: center;
139 | }
140 |
141 | /* Image styling to ensure proper fit in circle */
142 | .profile-circle img {
143 | width: 100%;
144 | height: 100%;
145 | object-fit: cover; /* Ensures image covers area without distortion */
146 | }
147 |
148 | /* About section styling - white background with padding */
149 | .about-section {
150 | padding: 5rem 5%;
151 | background-color: white;
152 | }
153 |
154 | /* About section heading */
155 | .about-section h2 {
156 | font-family: 'Playfair Display', serif;
157 | font-size: 2.5rem;
158 | text-align: center;
159 | margin-bottom: 3rem;
160 | color: var(--secondary);
161 | }
162 |
163 | /* Content container for about text */
164 | .about-content {
165 | max-width: 800px;
166 | margin: 0 auto;
167 | margin-bottom: 3rem; /* Reduced space between About and Approach sections */
168 | }
169 |
170 | /* Paragraph styling within about text */
171 | .about-text p {
172 | margin-bottom: 1.5rem;
173 | font-size: 1.1rem;
174 | }
175 |
176 | /* Coaching Approach / Benefits section styling */
177 | .benefits-section {
178 | margin-top: 2rem; /* Reduced space above the Approach section */
179 | padding-top: 1rem;
180 | border-top: 1px solid rgba(0,0,0,0.05); /* Subtle divider between sections */
181 | }
182 |
183 | /* Container for the 3-column layout */
184 | .benefits-container {
185 | display: flex;
186 | flex-wrap: wrap; /* Allows responsive column layout */
187 | justify-content: flex-start; /* Align to the left to create space on the right */
188 | gap: 2.5rem; /* Space between columns for better readability */
189 | max-width: 75%; /* Reduce the width to create space on the right */
190 | margin: 0 auto 0 0; /* Align container to the left */
191 | padding-left: 5%; /* Add padding to match other sections */
192 | }
193 |
194 | /* Individual benefit/approach card styling */
195 | .benefit-item {
196 | flex: 1;
197 | min-width: 300px;
198 | max-width: 350px;
199 | padding: 2rem;
200 | background-color: white;
201 | border-radius: 8px;
202 | box-shadow: 0 5px 15px rgba(0,0,0,0.05); /* Subtle shadow for depth */
203 | text-align: center;
204 | transition: transform 0.3s, box-shadow 0.3s; /* Smooth hover animation */
205 | margin: 0 auto; /* Center the cards */
206 | }
207 |
208 | /* Hover effect for benefit items - slight lift and enhanced shadow */
209 | .benefit-item:hover {
210 | transform: translateY(-5px);
211 | box-shadow: 0 8px 20px rgba(0,0,0,0.08);
212 | }
213 |
214 | /* Emoji icon styling for benefit items */
215 | .benefit-icon {
216 | font-size: 2.5rem;
217 | margin-bottom: 1rem;
218 | }
219 |
220 | /* Benefit item heading style */
221 | .benefit-item h3 {
222 | font-family: 'Playfair Display', serif;
223 | font-size: 1.5rem;
224 | margin-bottom: 1rem;
225 | color: var(--secondary);
226 | }
227 |
228 | /* Benefit item paragraph text */
229 | .benefit-item p {
230 | font-size: 1rem;
231 | color: var(--text);
232 | }
233 |
234 | /* Quote section with purple background */
235 | .quote-section {
236 | padding: 5rem 5%;
237 | background-color: rgb(131, 60, 163);
238 | color: white;
239 | text-align: center;
240 | }
241 |
242 | /* Quote styling - larger elegant font */
243 | .quote {
244 | max-width: 800px;
245 | margin: 0 auto 2rem;
246 | font-family: 'Playfair Display', serif;
247 | font-size: 1.8rem;
248 | font-style: italic;
249 | line-height: 1.4;
250 | }
251 |
252 | /* Quote paragraph styling */
253 | .quote p {
254 | margin-bottom: 1rem;
255 | }
256 |
257 | /* Quote attribution styling */
258 | .quote footer {
259 | font-size: 1.2rem;
260 | font-weight: 500;
261 | font-family: 'Montserrat', sans-serif;
262 | }
263 |
264 | /* Loading state text for quote */
265 | .loading {
266 | font-size: 1.2rem;
267 | margin-bottom: 2rem;
268 | }
269 |
270 | /* Error message styling */
271 | .error {
272 | color: var(--accent);
273 | margin-bottom: 1rem;
274 | font-weight: 500;
275 | }
276 |
277 | /* Quote refresh button - semi-transparent with border */
278 | .refresh-quote {
279 | background-color: rgba(34, 88, 211, 0.2);
280 | color: white;
281 | border: 2px solid white;
282 | padding: 0.5rem 1.5rem;
283 | border-radius: 4px;
284 | cursor: pointer;
285 | font-weight: 500;
286 | transition: background-color 0.3s; /* Smooth hover transition */
287 | }
288 |
289 | /* Refresh button hover state */
290 | .refresh-quote:hover {
291 | background-color: rgba(255, 255, 255, 0.3);
292 | }
--------------------------------------------------------------------------------