Revert to version 777bc71

This commit is contained in:
Flatlogic Bot 2026-02-17 23:52:44 +00:00
parent 2304edcdf3
commit 15445f0bc1
14 changed files with 2046 additions and 1903 deletions

View File

@ -23,7 +23,7 @@ module.exports = class BusinessesDBApi {
const limit = filter.limit || 0;
let offset = 0;
let where = {};
const currentPage = +filter.page || 0;
const currentPage = +filter.page;
offset = currentPage * limit;
const currentUser = options?.currentUser;
@ -42,67 +42,11 @@ module.exports = class BusinessesDBApi {
where.is_active = true;
}
let include = [
{ model: db.users, as: 'owner_user' },
{
model: db.business_photos,
as: 'business_photos_business',
include: [{
model: db.file,
as: 'photos'
}]
}
];
let include = [{ model: db.users, as: 'owner_user' }];
if (filter) {
if (filter.id) where.id = Utils.uuid(filter.id);
const searchConditions = [];
if (filter.name) {
const terms = filter.name.split(' ').filter(t => t.length > 0);
if (terms.length > 0) {
const termConditions = terms.map(term => ({
[Op.or]: [
{ name: { [Op.iLike]: `%${term}%` } },
{ description: { [Op.iLike]: `%${term}%` } },
{ address: { [Op.iLike]: `%${term}%` } },
{ city: { [Op.iLike]: `%${term}%` } },
{ zip: { [Op.iLike]: `%${term}%` } }
]
}));
searchConditions.push({ [Op.and]: termConditions });
}
}
if (filter.city || filter.zip) {
const location = filter.city || filter.zip;
const terms = location.split(' ').filter(t => t.length > 0);
if (terms.length > 0) {
const termConditions = terms.map(term => ({
[Op.or]: [
{ city: { [Op.iLike]: `%${term}%` } },
{ zip: { [Op.iLike]: `%${term}%` } },
{ address: { [Op.iLike]: `%${term}%` } }
]
}));
searchConditions.push({ [Op.and]: termConditions });
}
}
if (searchConditions.length > 0) {
where[Op.and] = searchConditions;
}
if (filter.is_active) where.is_active = filter.is_active === 'true';
if (filter.category) {
include.push({
model: db.business_categories,
as: 'business_categories_business',
where: { categoryId: filter.category }
});
}
if (filter.name) where.name = { [Op.iLike]: `%${filter.name}%` };
}
const queryOptions = {

View File

@ -1,9 +1,11 @@
import React from 'react'
import { mdiClose, mdiMagnify } from '@mdi/js'
import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import BaseIcon from './BaseIcon'
import Link from 'next/link';
type Props = {
menu: MenuAsideItem[]
@ -12,50 +14,50 @@ type Props = {
}
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
const { asideStyle, asideBrandStyle, asideScrollbarsStyle } = useAppSelector(
(state) => state.style
)
const corners = useAppSelector((state) => state.style.corners);
const asideStyle = useAppSelector((state) => state.style.asideStyle)
const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
const darkMode = useAppSelector((state) => state.style.darkMode)
const handleAsideLgCloseClick = (e: React.MouseEvent) => {
e.preventDefault()
props.onAsideLgCloseClick()
}
return (
<aside
className={`${className} z-40 w-64 fixed flex flex-col h-screen transition-all duration-300 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800`}
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
>
<div
className={`${asideBrandStyle} flex flex-row w-full flex-1 h-14 items-center justify-between px-6 py-8`}
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<div className="flex-1 flex items-center group cursor-pointer">
<div className="w-8 h-8 bg-emerald-500 rounded-lg flex items-center justify-center mr-3 shadow-lg shadow-emerald-500/20 group-hover:scale-110 transition-transform">
<BaseIcon path={mdiMagnify} size={20} className="text-slate-900" />
</div>
<b className="font-black text-slate-900 dark:text-white">Crafted Network</b>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
onClick={handleAsideLgCloseClick}
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<BaseIcon path={mdiClose} size={24} />
</button>
</div>
<div
className={`flex-1 overflow-y-auto overflow-x-hidden ${asideScrollbarsStyle}`}
>
<AsideMenuList menu={menu} />
</div>
<div className="p-6 border-t border-slate-100 dark:border-slate-800">
<div className="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-2xl border border-slate-100 dark:border-slate-700">
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-1">System Status</p>
<div className="flex items-center text-emerald-500 text-xs font-black uppercase">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mr-2 animate-pulse" />
Verified Network
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">Crafted Network</b>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
onClick={handleAsideLgCloseClick}
>
<BaseIcon path={mdiClose} />
</button>
</div>
<div
className={`flex-1 overflow-y-auto overflow-x-hidden ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<AsideMenuList menu={menu} />
</div>
</div>
</aside>
)
}
}

View File

@ -1,238 +1,221 @@
import dayjs from 'dayjs';
import _ from 'lodash';
export const filesFormatter = (arr) => {
if (!arr || !arr.length) return [];
return arr.map((item) => item);
};
export const imageFormatter = (arr) => {
if (!arr || !arr.length) return []
return arr.map(item => ({
publicUrl: item.publicUrl || ''
}))
};
export const oneImageFormatter = (arr) => {
if (!arr || !arr.length) return ''
return arr[0].publicUrl || ''
};
export const dateFormatter = (date) => {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
};
export const dateTimeFormatter = (date) => {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD HH:mm')
};
export const booleanFormatter = (val) => {
return val ? 'Yes' : 'No'
};
export const dataGridEditFormatter = (obj) => {
return _.transform(obj, (result, value, key) => {
if (_.isArray(value)) {
result[key] = _.map(value, 'id');
} else if (_.isObject(value)) {
result[key] = value.id;
} else {
result[key] = value;
}
});
};
export const usersManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.firstName)
};
export const usersOneListFormatter = (val) => {
if (!val) return ''
return val.firstName
};
export const usersManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.firstName}
});
};
export const usersOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.firstName, id: val.id}
};
export const rolesManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.name)
};
export const rolesOneListFormatter = (val) => {
if (!val) return ''
return val.name
};
export const rolesManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
};
export const rolesOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.name, id: val.id}
};
export const permissionsManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.name)
};
export const permissionsOneListFormatter = (val) => {
if (!val) return ''
return val.name
};
export const permissionsManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
};
export const permissionsOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.name, id: val.id}
};
export const categoriesManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.name)
};
export const categoriesOneListFormatter = (val) => {
if (!val) return ''
return val.name
};
export const categoriesManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
};
export const categoriesOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.name, id: val.id}
};
export const businessesManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.name)
};
export const businessesOneListFormatter = (val) => {
if (!val) return ''
return val.name
};
export const businessesManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
};
export const businessesOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.name, id: val.id}
};
export const verification_submissionsManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.notes)
};
export const verification_submissionsOneListFormatter = (val) => {
if (!val) return ''
return val.notes
};
export const verification_submissionsManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.notes}
});
};
export const verification_submissionsOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.notes, id: val.id}
};
export const leadsManyListFormatter = (val) => {
if (!val || !val.length) return []
return val.map((item) => item.keyword)
};
export const leadsOneListFormatter = (val) => {
if (!val) return ''
return val.keyword
};
export const leadsManyListFormatterEdit = (val) => {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.keyword}
});
};
export const leadsOneListFormatterEdit = (val) => {
if (!val) return ''
return {label: val.keyword, id: val.id}
};
// Also keep the default export for compatibility
export default {
filesFormatter,
imageFormatter,
oneImageFormatter,
dateFormatter,
dateTimeFormatter,
booleanFormatter,
dataGridEditFormatter,
usersManyListFormatter,
usersOneListFormatter,
usersManyListFormatterEdit,
usersOneListFormatterEdit,
rolesManyListFormatter,
rolesOneListFormatter,
rolesManyListFormatterEdit,
rolesOneListFormatterEdit,
permissionsManyListFormatter,
permissionsOneListFormatter,
permissionsManyListFormatterEdit,
permissionsOneListFormatterEdit,
categoriesManyListFormatter,
categoriesOneListFormatter,
categoriesManyListFormatterEdit,
categoriesOneListFormatterEdit,
businessesManyListFormatter,
businessesOneListFormatter,
businessesManyListFormatterEdit,
businessesOneListFormatterEdit,
verification_submissionsManyListFormatter,
verification_submissionsOneListFormatter,
verification_submissionsManyListFormatterEdit,
verification_submissionsOneListFormatterEdit,
leadsManyListFormatter,
leadsOneListFormatter,
leadsManyListFormatterEdit,
leadsOneListFormatterEdit,
};
filesFormatter(arr) {
if (!arr || !arr.length) return [];
return arr.map((item) => item);
},
imageFormatter(arr) {
if (!arr || !arr.length) return []
return arr.map(item => ({
publicUrl: item.publicUrl || ''
}))
},
oneImageFormatter(arr) {
if (!arr || !arr.length) return ''
return arr[0].publicUrl || ''
},
dateFormatter(date) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD')
},
dateTimeFormatter(date) {
if (!date) return ''
return dayjs(date).format('YYYY-MM-DD HH:mm')
},
booleanFormatter(val) {
return val ? 'Yes' : 'No'
},
dataGridEditFormatter(obj) {
return _.transform(obj, (result, value, key) => {
if (_.isArray(value)) {
result[key] = _.map(value, 'id');
} else if (_.isObject(value)) {
result[key] = value.id;
} else {
result[key] = value;
}
});
},
usersManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.firstName)
},
usersOneListFormatter(val) {
if (!val) return ''
return val.firstName
},
usersManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.firstName}
});
},
usersOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.firstName, id: val.id}
},
rolesManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.name)
},
rolesOneListFormatter(val) {
if (!val) return ''
return val.name
},
rolesManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
},
rolesOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.name, id: val.id}
},
permissionsManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.name)
},
permissionsOneListFormatter(val) {
if (!val) return ''
return val.name
},
permissionsManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
},
permissionsOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.name, id: val.id}
},
categoriesManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.name)
},
categoriesOneListFormatter(val) {
if (!val) return ''
return val.name
},
categoriesManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
},
categoriesOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.name, id: val.id}
},
businessesManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.name)
},
businessesOneListFormatter(val) {
if (!val) return ''
return val.name
},
businessesManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.name}
});
},
businessesOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.name, id: val.id}
},
verification_submissionsManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.notes)
},
verification_submissionsOneListFormatter(val) {
if (!val) return ''
return val.notes
},
verification_submissionsManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.notes}
});
},
verification_submissionsOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.notes, id: val.id}
},
leadsManyListFormatter(val) {
if (!val || !val.length) return []
return val.map((item) => item.keyword)
},
leadsOneListFormatter(val) {
if (!val) return ''
return val.keyword
},
leadsManyListFormatterEdit(val) {
if (!val || !val.length) return []
return val.map((item) => {
return {id: item.id, label: item.keyword}
});
},
leadsOneListFormatterEdit(val) {
if (!val) return ''
return {label: val.keyword, id: val.id}
},
}

View File

@ -1,176 +1,116 @@
import React, { ReactNode, useState, useEffect } from 'react'
import Link from 'next/link'
import { mdiMenu, mdiClose, mdiLogin, mdiAccountPlus, mdiMagnify } from '@mdi/js'
import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router'
import BaseIcon from '../components/BaseIcon'
import React, { ReactNode } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { mdiShieldCheck, mdiMenu, mdiClose, mdiMagnify } from '@mdi/js';
import { useAppSelector } from '../stores/hooks';
import BaseIcon from '../components/BaseIcon';
type Props = {
children: ReactNode
}
export default function LayoutGuest({ children }: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const { token } = useAppSelector((state) => state.auth)
const router = useRouter()
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
const darkMode = useAppSelector((state) => state.style.darkMode);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const { currentUser } = useAppSelector((state) => state.auth);
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
const router = useRouter();
const navLinks = [
{ label: 'Find Services', href: '/search' },
{ label: 'Categories', href: '/categories' },
{ label: 'Verify Business', href: '/register' },
{ label: 'Support', href: '/contact-form' },
]
{ href: '/search', label: 'Find Services' },
{ href: '/register', label: 'List Business' },
];
return (
<div className="min-h-screen bg-white dark:bg-slate-950 flex flex-col font-sans selection:bg-emerald-500/30">
{/* Navigation */}
<nav
className={`fixed top-0 left-0 right-0 z-[100] transition-all duration-300 ${
scrolled
? 'bg-white/80 dark:bg-slate-900/80 backdrop-blur-xl border-b border-slate-200 dark:border-slate-800 py-4 shadow-sm'
: 'bg-transparent py-6'
}`}
>
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
<Link href="/" className="flex items-center group">
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center mr-3 shadow-lg shadow-emerald-500/20 group-hover:scale-110 transition-transform">
<BaseIcon path={mdiMagnify} size={24} className="text-slate-900" />
<div className={`${darkMode ? 'dark' : ''} min-h-screen flex flex-col`}>
{/* Dynamic Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-lg border-b border-slate-200/60 dark:bg-slate-900/80 dark:border-slate-800">
<div className="container mx-auto px-6 h-20 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3 group">
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20 group-hover:scale-110 transition-transform">
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
</div>
<span className="text-xl font-black tracking-tight dark:text-white">Crafted Network<span className="text-emerald-500 italic"></span></span>
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center space-x-8">
<nav className="hidden md:flex items-center gap-10">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-sm font-bold text-slate-600 dark:text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
href={link.href}
className={`text-sm font-bold uppercase tracking-widest hover:text-emerald-500 transition-colors ${router.pathname === link.href ? 'text-emerald-500' : 'text-slate-600 dark:text-slate-400'}`}
>
{link.label}
</Link>
))}
</div>
<div className="hidden md:flex items-center space-x-4">
{token ? (
<Link
href="/dashboard"
className="bg-slate-900 dark:bg-emerald-500 text-white dark:text-slate-900 px-6 py-2.5 rounded-xl text-sm font-bold hover:opacity-90 transition-all shadow-lg"
>
<div className="h-6 w-px bg-slate-200 dark:bg-slate-800"></div>
{currentUser ? (
<Link href="/dashboard" className="bg-slate-900 text-white dark:bg-white dark:text-slate-900 px-6 py-3 rounded-xl text-sm font-bold hover:shadow-xl transition-all">
Go to Dashboard
</Link>
) : (
<>
<Link
href="/login"
className="text-sm font-bold text-slate-600 dark:text-slate-400 hover:text-emerald-600 transition-colors px-4 py-2"
>
Log In
<div className="flex items-center gap-4">
<Link href="/login" className="text-sm font-bold text-slate-600 hover:text-emerald-500 transition-colors">
Login
</Link>
<Link
href="/register"
className="bg-emerald-500 text-slate-900 px-6 py-2.5 rounded-xl text-sm font-bold hover:shadow-lg hover:shadow-emerald-500/20 transition-all active:scale-95"
>
Join the Network
<Link href="/register" className="bg-emerald-500 text-white px-6 py-3 rounded-xl text-sm font-bold hover:bg-emerald-600 shadow-lg shadow-emerald-500/20 transition-all">
Join Now
</Link>
</>
</div>
)}
</div>
</nav>
{/* Mobile Toggle */}
<button
className="md:hidden p-2 text-slate-600 dark:text-slate-400"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<button className="md:hidden p-2 text-slate-600 dark:text-slate-400" onClick={() => setIsMenuOpen(!isMenuOpen)}>
<BaseIcon path={isMenuOpen ? mdiClose : mdiMenu} size={28} />
</button>
</div>
{/* Mobile Menu */}
{isMenuOpen && (
<div className="md:hidden bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 p-6 space-y-6 animate-fade-in">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="block text-lg font-bold text-slate-800 dark:text-slate-200"
onClick={() => setIsMenuOpen(false)}
>
<Link key={link.href} href={link.href} className="block text-lg font-bold text-slate-700 dark:text-slate-300" onClick={() => setIsMenuOpen(false)}>
{link.label}
</Link>
))}
<div className="pt-6 border-t border-slate-100 dark:border-slate-800 flex flex-col gap-4">
<Link
href="/login"
className="flex items-center justify-center gap-2 p-4 rounded-xl border border-slate-200 dark:border-slate-700 font-bold"
onClick={() => setIsMenuOpen(false)}
>
<BaseIcon path={mdiLogin} size={20} /> Log In
</Link>
<Link
href="/register"
className="flex items-center justify-center gap-2 p-4 rounded-xl bg-emerald-500 text-slate-900 font-bold shadow-lg"
onClick={() => setIsMenuOpen(false)}
>
<BaseIcon path={mdiAccountPlus} size={20} /> Join Now
</Link>
{currentUser ? (
<Link href="/dashboard" className="w-full bg-slate-900 text-white py-4 rounded-xl text-center font-bold" onClick={() => setIsMenuOpen(false)}>Dashboard</Link>
) : (
<>
<Link href="/login" className="w-full text-center py-4 font-bold text-slate-600" onClick={() => setIsMenuOpen(false)}>Login</Link>
<Link href="/register" className="w-full bg-emerald-500 text-white py-4 rounded-xl text-center font-bold" onClick={() => setIsMenuOpen(false)}>Join Now</Link>
</>
)}
</div>
</div>
)}
</nav>
</header>
{/* Main Content */}
<main className="flex-grow pt-16">
<main className={`flex-grow ${bgColor} dark:bg-slate-800 dark:text-slate-100`}>
{children}
</main>
{/* Footer */}
<footer className="bg-slate-900 text-white pt-24 pb-12">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div className="col-span-1 md:col-span-2 space-y-6">
<div className="flex items-center">
<div className="w-8 h-8 bg-emerald-500 rounded-lg flex items-center justify-center mr-3">
<BaseIcon path={mdiMagnify} size={20} className="text-slate-900" />
</div>
<span className="text-2xl font-bold tracking-tight dark:text-white text-white">Crafted Network</span>
<footer className="bg-white border-t border-slate-200 py-12 dark:bg-slate-900 dark:border-slate-800">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center mb-6 md:mb-0">
<div className="w-10 h-10 bg-emerald-500 rounded-xl flex items-center justify-center mr-3">
<BaseIcon path={mdiShieldCheck} size={24} className="text-white" />
</div>
<p className="text-slate-400 max-w-sm leading-relaxed">
The most reliable platform for finding verified service professionals and businesses across all industries.
</p>
<span className="text-2xl font-bold tracking-tight dark:text-white text-slate-900">Crafted Network</span>
</div>
<div>
<h4 className="font-bold mb-6 text-white uppercase text-xs tracking-widest">Platform</h4>
<ul className="space-y-4 text-slate-400 text-sm">
<li><Link href="/search" className="hover:text-emerald-400 transition-colors">Find Services</Link></li>
<li><Link href="/categories" className="hover:text-emerald-400 transition-colors">Categories</Link></li>
<li><Link href="/register" className="hover:text-emerald-400 transition-colors">Register Business</Link></li>
</ul>
</div>
<div>
<h4 className="font-bold mb-6 text-white uppercase text-xs tracking-widest">Legal</h4>
<ul className="space-y-4 text-slate-400 text-sm">
<li><Link href="/privacy-policy" className="hover:text-emerald-400 transition-colors">Privacy Policy</Link></li>
<li><Link href="/terms-of-use" className="hover:text-emerald-400 transition-colors">Terms of Use</Link></li>
<li><Link href="/contact-form" className="hover:text-emerald-400 transition-colors">Contact Support</Link></li>
</ul>
<div className="flex gap-8 text-slate-500 font-medium dark:text-slate-400">
<Link href="/search" className="hover:text-emerald-500">Find Help</Link>
<Link href="/register" className="hover:text-emerald-500">List Business</Link>
<Link href="/privacy-policy" className="hover:text-emerald-500">Privacy</Link>
<Link href="/terms-of-use" className="hover:text-emerald-500">Terms</Link>
</div>
</div>
<div className="pt-12 border-t border-slate-800 text-center text-slate-500 text-sm">
<div className="mt-12 pt-8 border-t border-slate-100 dark:border-slate-800 text-center text-slate-400 text-sm">
© 2026 Crafted Network. Built with Trust & Transparency.
</div>
</div>
</footer>
</div>
)
}
}

View File

@ -1,116 +1,201 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
import React from 'react';
import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { Provider } from 'react-redux';
import { appWithTranslation } from 'next-i18next';
import axios from 'axios';
import { store } from '../stores/store';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { findMe, logoutUser } from '../stores/authSlice';
import { setDarkMode } from '../stores/styleSlice';
import { Provider } from 'react-redux';
import '../css/main.css';
import '../css/_calendar.css';
import axios from 'axios';
import { baseURLApi } from '../config';
import { useRouter } from 'next/router';
import ErrorBoundary from "../components/ErrorBoundary";
import DevModeBadge from '../components/DevModeBadge';
import 'intro.js/introjs.css';
import { appWithTranslation } from 'next-i18next';
import '../i18n';
import IntroGuide from '../components/IntroGuide';
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
type NextPageWithLayout = {
getLayout?: (page: ReactElement) => ReactNode;
};
// Initialize axios
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
? process.env.NEXT_PUBLIC_BACK_API
: baseURLApi;
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
// Axios Configuration
const baseURLApi = process.env.NEXT_PUBLIC_BACK_API || '/api';
axios.defaults.baseURL = baseURLApi;
axios.defaults.headers.common['Content-Type'] = 'application/json';
const AppContent = ({ Component, pageProps }: AppPropsWithLayout) => {
const router = useRouter();
const dispatch = useAppDispatch();
const { token, currentUser } = useAppSelector((state) => state.auth);
const { darkMode } = useAppSelector((state) => state.style);
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
// Auth Header Interceptor
useEffect(() => {
const requestInterceptor = axios.interceptors.request.use(
(config) => {
const currentToken = localStorage.getItem('token');
if (currentToken) {
config.headers.Authorization = `Bearer ${currentToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
const responseInterceptor = axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
dispatch(logoutUser());
if (!router.pathname.startsWith('/login') && !router.pathname.startsWith('/register') && !router.pathname.startsWith('/public')) {
router.push('/login');
}
}
return Promise.reject(error);
}
);
return () => {
axios.interceptors.request.eject(requestInterceptor);
axios.interceptors.response.eject(responseInterceptor);
};
}, [dispatch, router]);
// Initial Data Fetch
useEffect(() => {
const savedToken = localStorage.getItem('token');
if (savedToken) {
dispatch(findMe());
}
}, [dispatch]);
// Dark Mode Support
useEffect(() => {
const isDark = localStorage.getItem('darkMode') === 'true';
dispatch(setDarkMode(isDark));
if (isDark) {
document.documentElement.classList.add('dark-scrollbars');
}
}, [dispatch]);
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [darkMode]);
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout || ((page) => page);
const router = useRouter();
const [stepsEnabled, setStepsEnabled] = React.useState(false);
const [stepName, setStepName] = React.useState('');
const [steps, setSteps] = React.useState([]);
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
} else {
delete config.headers.Authorization;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// TODO: Remove this code in future releases
React.useEffect(() => {
const allowedOrigin = (() => {
if (!document.referrer) {
return null;
}
try {
return new URL(document.referrer).origin;
} catch (error) {
console.warn('[postMessage] Failed to parse parent origin from referrer', error);
return null;
}
})();
const handleMessage = async (event: MessageEvent) => {
if (event.data === 'getLocation') {
event.source?.postMessage(
{ iframeLocation: window.location.pathname },
event.origin,
);
return;
}
if (event.data === 'getAuthToken') {
if (allowedOrigin && event.origin !== allowedOrigin) {
console.warn('[postMessage] Blocked getAuthToken from origin', event.origin);
return;
}
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
event.source?.postMessage(
{ iframeAuthToken: token, iframeAuthUser: user },
event.origin,
);
return;
}
if (event.data === 'getScreenshot') {
try {
const html2canvas = (await import('html2canvas')).default;
const canvas = await html2canvas(document.body, { useCORS: true });
const url = canvas.toDataURL('image/jpeg', 0.8);
event.source?.postMessage({ iframeScreenshot: url }, event.origin);
} catch (e) {
console.error('html2canvas failed', e);
event.source?.postMessage({ iframeScreenshot: null }, event.origin);
}
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
React.useEffect(() => {
// Tour is disabled by default in generated projects.
return;
const isCompleted = (stepKey: string) => {
return localStorage.getItem(`completed_${stepKey}`) === 'true';
};
if (router.pathname === '/login' && !isCompleted('loginSteps')) {
setSteps(loginSteps);
setStepName('loginSteps');
setStepsEnabled(true);
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
setTimeout(() => {
setSteps(appSteps);
setStepName('appSteps');
setStepsEnabled(true);
}, 1000);
} else if (router.pathname === '/users/users-list' && !isCompleted('usersSteps')) {
setTimeout(() => {
setSteps(usersSteps);
setStepName('usersSteps');
setStepsEnabled(true);
}, 1000);
} else if (router.pathname === '/roles/roles-list' && !isCompleted('rolesSteps')) {
setTimeout(() => {
setSteps(rolesSteps);
setStepName('rolesSteps');
setStepsEnabled(true);
}, 1000);
} else {
setSteps([]);
setStepsEnabled(false);
}
}, [router.pathname]);
const handleExit = () => {
setStepsEnabled(false);
};
const title = 'Crafted Network'
const description = "Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking."
const url = "https://flatlogic.com/"
const image = "https://project-screens.s3.amazonaws.com/screenshots/38501/app-hero-20260217-010030.png"
const imageWidth = '1920'
const imageHeight = '960'
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
</Head>
{getLayout(<Component {...pageProps} />)}
</>
);
};
function MyApp(props: AppPropsWithLayout) {
return (
<Provider store={store}>
<AppContent {...props} />
{getLayout(
<>
<Head>
<meta name="description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:site_name" content="https://flatlogic.com/" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content={imageWidth} />
<meta property="og:image:height" content={imageHeight} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image:src" content={image} />
<meta property="twitter:image:width" content={imageWidth} />
<meta property="twitter:image:height" content={imageHeight} />
<link rel="icon" href="/favicon.svg" />
</Head>
<ErrorBoundary>
<Component {...pageProps} />
</ErrorBoundary>
<IntroGuide
steps={steps}
stepsName={stepName}
stepsEnabled={stepsEnabled}
onExit={handleExit}
/>
{(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'dev_stage') && <DevModeBadge />}
</>
)}
</Provider>
);
)
}
export default appWithTranslation(MyApp);

View File

@ -1,275 +1,192 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import {
mdiMagnify,
mdiMapMarker,
mdiChevronRight,
mdiStar,
mdiShieldCheck,
mdiLightningBolt,
mdiFormatListBulleted,
mdiAccountGroup
mdiCurrencyUsd,
mdiFlash,
mdiTools,
mdiPowerPlug,
mdiAirConditioner,
mdiBrush,
mdiFormatPaint
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import SectionMain from '../components/SectionMain';
import LayoutGuest from '../layouts/Guest';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchCategories } from '../stores/categories/categoriesSlice';
import { fetch as fetchBusinesses } from '../stores/businesses/businessesSlice';
import type { ReactElement } from 'react';
const LandingPage = () => {
export default function LandingPage() {
const router = useRouter();
const dispatch = useAppDispatch();
const { categories } = useAppSelector((state) => state.categories);
const { businesses: featuredBusinesses } = useAppSelector((state) => state.businesses);
const [searchQuery, setSearchQuery] = useState('');
const [locationQuery, setLocationQuery] = useState('');
const { currentUser } = useAppSelector((state) => state.auth);
useEffect(() => {
dispatch(fetchCategories({ query: '?limit=8' }));
dispatch(fetchBusinesses({ query: '?limit=4&is_active=true' }));
}, [dispatch]);
const handleSearch = () => {
const featuredCategories = [
{ name: 'Plumbing', icon: mdiTools, color: 'text-blue-500' },
{ name: 'Electrical', icon: mdiPowerPlug, color: 'text-yellow-500' },
{ name: 'HVAC', icon: mdiAirConditioner, color: 'text-emerald-500' },
{ name: 'Cleaning', icon: mdiBrush, color: 'text-purple-500' },
{ name: 'Painting', icon: mdiFormatPaint, color: 'text-orange-500' },
{ name: 'General', icon: mdiTools, color: 'text-slate-500' },
];
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const query = formData.get('query');
const location = formData.get('location');
router.push({
pathname: '/search',
query: { q: searchQuery, l: locationQuery },
query: { query, location },
});
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
};
const stats = [
{ label: 'Verified Pros', value: '12,000+', icon: mdiShieldCheck, color: 'text-emerald-500' },
{ label: 'Services Listed', value: '850+', icon: mdiFormatListBulleted, color: 'text-blue-500' },
{ label: 'Happy Customers', value: '95k', icon: mdiAccountGroup, color: 'text-purple-500' },
{ label: 'Avg. Rating', value: '4.9/5', icon: mdiStar, color: 'text-amber-500' },
];
const features = [
{
title: 'Verified Ownership',
desc: 'Every business on our platform goes through a rigorous multi-step verification process to ensure authenticity.',
icon: mdiShieldCheck,
color: 'bg-emerald-100 text-emerald-600'
},
{
title: 'Smart Matching',
desc: 'Our AI-powered engine connects you with the right professionals based on your specific needs and location.',
icon: mdiLightningBolt,
color: 'bg-blue-100 text-blue-600'
},
{
title: 'Price Transparency',
desc: 'View clear pricing and service breakdowns before you book. No hidden fees, no surprises.',
icon: mdiStar,
color: 'bg-amber-100 text-amber-600'
}
];
return (
<>
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
<Head>
<title>Crafted Network | 21st Century Service Directory</title>
<meta name="description" content="Connect with verified service professionals. Trust, transparency, and AI-powered matching." />
</Head>
{/* Hero Section */}
<section className="relative bg-slate-900 pt-32 pb-48 overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 opacity-20">
<div className="absolute -top-[10%] -left-[10%] w-[40%] h-[40%] bg-emerald-500 blur-[120px] rounded-full"></div>
<div className="absolute top-[20%] -right-[5%] w-[30%] h-[30%] bg-blue-600 blur-[100px] rounded-full"></div>
<section className="relative bg-slate-900 text-white overflow-hidden py-32 lg:py-48">
<div className="absolute inset-0 opacity-20">
<div className="absolute top-0 -left-4 w-72 h-72 bg-emerald-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div>
<div className="absolute top-0 -right-4 w-72 h-72 bg-blue-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-500 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-4000"></div>
</div>
<SectionMain className="relative z-10">
<div className="text-center max-w-4xl mx-auto space-y-8">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-slate-800 border border-slate-700 text-emerald-400 text-sm font-medium mb-4 animate-fade-in">
<div className="container mx-auto px-6 relative z-10">
<div className="text-center max-w-4xl mx-auto">
<div className="inline-flex items-center px-4 py-2 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm font-medium mb-8">
<BaseIcon path={mdiShieldCheck} size={18} className="mr-2" />
The World&apos;s Most Trusted Professional Network
Verified Professionals & AI-Powered Matching
</div>
<h1 className="text-5xl md:text-7xl font-black text-white leading-tight tracking-tight">
The <span className="text-emerald-400">Crafted</span> Network
<h1 className="text-5xl lg:text-7xl font-bold tracking-tight mb-8">
The <span className="text-emerald-400">Crafted</span> Service Network
</h1>
<p className="text-xl text-slate-400 leading-relaxed max-w-2xl mx-auto">
<p className="text-xl text-slate-400 mb-12 max-w-2xl mx-auto leading-relaxed">
Find reliable, verified experts for your home or business. Real-time availability, transparent pricing, and zero spam.
</p>
{/* Search Bar */}
<div className="flex flex-col md:flex-row gap-4 max-w-3xl mx-auto mt-12 bg-slate-800/50 p-3 rounded-2xl border border-slate-700/50 backdrop-blur-xl shadow-2xl">
<div className="flex-1 relative group">
<BaseIcon path={mdiMagnify} size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-400 transition-colors" />
<form onSubmit={handleSearch} className="flex flex-col md:flex-row gap-4 p-2 bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 shadow-2xl max-w-3xl mx-auto">
<div className="flex-grow relative">
<BaseIcon path={mdiMagnify} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
name="query"
placeholder="What service do you need?"
className="w-full bg-transparent border-none focus:ring-0 py-4 pl-12 pr-4 text-white placeholder-slate-500 rounded-xl"
/>
</div>
<div className="w-px bg-slate-700 hidden md:block"></div>
<div className="flex-1 relative group">
<BaseIcon path={mdiMapMarker} size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-400 transition-colors" />
<div className="md:w-px h-8 bg-white/10 my-auto"></div>
<div className="flex-grow relative">
<BaseIcon path={mdiMapMarker} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={locationQuery}
onChange={(e) => setLocationQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="City or Zip code"
name="location"
placeholder="Location"
className="w-full bg-transparent border-none focus:ring-0 py-4 pl-12 pr-4 text-white placeholder-slate-500 rounded-xl"
/>
</div>
<button
onClick={handleSearch}
className="bg-emerald-500 hover:bg-emerald-400 text-slate-900 font-bold px-8 py-4 rounded-xl transition-all shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center justify-center"
>
Search
<button type="submit" className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-4 px-8 rounded-xl transition-all shadow-lg shadow-emerald-500/25">
Find Help
</button>
</div>
</form>
</div>
</SectionMain>
</div>
</section>
{/* Stats Section */}
<section className="-mt-24 relative z-20 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, idx) => (
<div key={idx} className="bg-white p-8 rounded-3xl shadow-xl shadow-slate-200/50 border border-slate-100 flex flex-col items-center text-center group hover:-translate-y-1 transition-all duration-300">
<div className={`${stat.color} mb-4 p-3 bg-slate-50 rounded-2xl group-hover:scale-110 transition-transform`}>
<BaseIcon path={stat.icon} size={24} />
{/* Featured Categories */}
<section className="py-24 container mx-auto px-6">
<div className="flex items-end justify-between mb-12">
<div>
<h2 className="text-3xl font-bold mb-4">Popular Services</h2>
<p className="text-slate-500">Explore our most requested categories from verified pros.</p>
</div>
<Link href="/categories/categories-list" className="text-emerald-500 font-semibold hover:underline flex items-center">
View All Categories
</Link>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
{(categories?.length > 0 ? categories.slice(0, 6) : featuredCategories).map((cat: any, i: number) => (
<Link
key={i}
href={`/search?query=${cat.name}`}
className="group bg-white p-8 rounded-3xl border border-slate-200 hover:border-emerald-500 hover:shadow-xl hover:shadow-emerald-500/10 transition-all text-center"
>
<div className={`mb-4 w-16 h-16 mx-auto rounded-2xl flex items-center justify-center bg-slate-50 group-hover:bg-emerald-50 transition-colors`}>
<BaseIcon path={cat.icon || mdiTools} size={32} className={cat.color || 'text-emerald-500'} />
</div>
<div className="text-3xl font-black text-slate-900 mb-1">{stat.value}</div>
<div className="text-sm font-medium text-slate-500">{stat.label}</div>
</div>
<span className="font-bold text-slate-700 group-hover:text-emerald-600 transition-colors">{cat.name}</span>
</Link>
))}
</div>
</section>
{/* Categories */}
<section className="py-32">
<SectionMain>
<div className="flex items-end justify-between mb-12">
<div>
<h2 className="text-3xl font-black text-slate-900 mb-4">Popular Categories</h2>
<p className="text-slate-500">Explore the best rated professionals in these top industries.</p>
</div>
<Link href="/categories" className="hidden md:flex items-center text-emerald-600 font-bold hover:gap-2 transition-all">
View All <BaseIcon path={mdiChevronRight} size={20} />
</Link>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{categories?.map((cat: any) => (
<Link key={cat.id} href={`/search?category=${cat.id}`} className="group bg-slate-50 hover:bg-white p-8 rounded-3xl border border-transparent hover:border-emerald-100 hover:shadow-xl hover:shadow-emerald-500/10 transition-all text-center">
<div className="w-16 h-16 bg-white rounded-2xl shadow-sm mx-auto mb-6 flex items-center justify-center text-emerald-500 group-hover:bg-emerald-500 group-hover:text-white transition-all">
<BaseIcon path={mdiStar} size={24} />
</div>
<span className="font-bold text-slate-700 group-hover:text-emerald-600 transition-colors">{cat.name}</span>
</Link>
))}
</div>
</SectionMain>
</section>
{/* Featured Businesses */}
<section className="py-32 bg-slate-50">
<SectionMain>
<div className="text-center mb-16 max-w-3xl mx-auto">
<h2 className="text-4xl font-black text-slate-900 mb-6">Verified Top Performers</h2>
<p className="text-lg text-slate-600 italic">&quot;Trust is earned through consistent, high-quality results.&quot;</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{featuredBusinesses?.map((biz: any) => (
<Link key={biz.id} href={`/public/businesses-details?id=${biz.id}`} className="bg-white rounded-[2rem] overflow-hidden shadow-sm hover:shadow-2xl transition-all group border border-slate-100">
<div className="h-56 bg-slate-200 relative overflow-hidden">
{biz.business_photos?.[0]?.photo_url ? (
<img src={biz.business_photos[0].photo_url} alt={biz.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-100 text-slate-300">
<BaseIcon path={mdiStar} size={48} />
</div>
)}
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur px-3 py-1 rounded-full text-xs font-black text-emerald-600 flex items-center shadow-sm">
<BaseIcon path={mdiStar} size={16} className="mr-1" />
{biz.rating || '4.9'}
</div>
</div>
<div className="p-8">
<h3 className="text-xl font-bold text-slate-900 mb-2 group-hover:text-emerald-600 transition-colors line-clamp-1">{biz.name}</h3>
<div className="flex items-center text-slate-500 text-sm mb-4">
<BaseIcon path={mdiMapMarker} size={16} className="mr-1" />
{biz.city || biz.locations?.[0]?.city || 'Verified Professional'}
</div>
<div className="flex items-center justify-between pt-4 border-t border-slate-50">
<span className="text-xs font-bold text-emerald-500 uppercase tracking-widest">Verified</span>
<BaseIcon path={mdiChevronRight} size={20} className="text-slate-300 group-hover:text-emerald-500 transition-colors" />
</div>
</div>
</Link>
))}
</div>
</SectionMain>
</section>
{/* Features */}
<section className="py-32">
<SectionMain>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{features.map((feature, idx) => (
<div key={idx} className="flex flex-col items-center text-center">
<div className={`w-16 h-16 ${feature.color} rounded-2xl flex items-center justify-center mb-6`}>
<BaseIcon path={feature.icon} size={32} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{feature.title}</h3>
<p className="text-slate-500 leading-relaxed">{feature.desc}</p>
{/* Trust Features */}
<section className="py-24 bg-slate-900 text-white overflow-hidden relative">
<div className="container mx-auto px-6 relative z-10">
<div className="grid lg:grid-cols-3 gap-12 text-center lg:text-left">
<div className="p-8 rounded-3xl bg-white/5 border border-white/10">
<div className="w-14 h-14 rounded-2xl bg-emerald-500 flex items-center justify-center mb-6 mx-auto lg:mx-0">
<BaseIcon path={mdiShieldCheck} size={28} />
</div>
))}
</div>
</SectionMain>
</section>
{/* CTA Section */}
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto bg-slate-900 rounded-[3rem] p-12 md:p-24 relative overflow-hidden text-center">
<div className="absolute top-0 left-0 w-full h-full overflow-hidden z-0 opacity-10">
<div className="absolute top-[50%] left-[50%] -translate-x-1/2 -translate-y-1/2 w-[80%] h-[80%] bg-emerald-500 blur-[150px] rounded-full"></div>
</div>
<div className="relative z-10 max-w-2xl mx-auto">
<h2 className="text-4xl md:text-5xl font-black text-white mb-8 leading-tight">
Ready to grow your professional service?
</h2>
<p className="text-xl text-slate-400 mb-12">
Join thousands of verified professionals and start connecting with quality leads today.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/auth/register" className="bg-emerald-500 hover:bg-emerald-400 text-slate-900 font-bold px-10 py-5 rounded-2xl transition-all shadow-xl shadow-emerald-500/20 active:scale-95">
Register Your Business
</Link>
<Link href="/contact" className="bg-slate-800 hover:bg-slate-700 text-white font-bold px-10 py-5 rounded-2xl transition-all border border-slate-700">
Contact Sales
</Link>
<h3 className="text-2xl font-bold mb-4">Verified Badges</h3>
<p className="text-slate-400 leading-relaxed">Every business undergoes a strict evidence-based verification process. Look for the shield to ensure peace of mind.</p>
</div>
<div className="p-8 rounded-3xl bg-white/5 border border-white/10">
<div className="w-14 h-14 rounded-2xl bg-blue-500 flex items-center justify-center mb-6 mx-auto lg:mx-0">
<BaseIcon path={mdiCurrencyUsd} size={28} />
</div>
<h3 className="text-2xl font-bold mb-4">Price Transparency</h3>
<p className="text-slate-400 leading-relaxed">No more hidden fees. See typical price ranges and median job costs upfront before you ever make a request.</p>
</div>
<div className="p-8 rounded-3xl bg-white/5 border border-white/10">
<div className="w-14 h-14 rounded-2xl bg-amber-500 flex items-center justify-center mb-6 mx-auto lg:mx-0">
<BaseIcon path={mdiFlash} size={28} />
</div>
<h3 className="text-2xl font-bold mb-4">AI Smart Matching</h3>
<p className="text-slate-400 leading-relaxed">Our matching engine analyzes your issue and finds the best expert based on availability, score, and proximity.</p>
</div>
</div>
</div>
</section>
</>
{/* Call to Action */}
<section className="py-24 container mx-auto px-6">
<div className="bg-emerald-500 rounded-[3rem] p-12 lg:p-20 text-white relative overflow-hidden flex flex-col lg:flex-row items-center justify-between">
<div className="relative z-10 max-w-2xl text-center lg:text-left mb-10 lg:mb-0">
<h2 className="text-4xl lg:text-5xl font-bold mb-6">Are you a service professional?</h2>
<p className="text-emerald-100 text-lg mb-0">Join the most trusted network of professionals and get high-quality leads that actually match your expertise.</p>
</div>
<div className="relative z-10 flex gap-4">
<Link href="/register" className="bg-slate-900 hover:bg-black text-white font-bold py-5 px-10 rounded-2xl transition-all shadow-2xl">
Register Business
</Link>
{!currentUser && (
<Link href="/login" className="bg-white hover:bg-slate-100 text-emerald-600 font-bold py-5 px-10 rounded-2xl transition-all">
Login
</Link>
)}
</div>
</div>
</section>
</div>
);
};
}
LandingPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default LandingPage;
};

View File

@ -1,190 +1,276 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik';
import { mdiAccount, mdiAsterisk, mdiShieldCheck, mdiArrowLeft } from '@mdi/js';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { loginUser, resetAction, findMe } from '../stores/authSlice';
import FormCheckRadio from '../components/FormCheckRadio';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
import { findMe, loginUser, resetAction } from '../stores/authSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
const validate = (values: any) => {
const errors: any = {};
if (!values.email) {
errors.email = 'Required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
}
return errors;
};
export default function LoginPage() {
export default function Login() {
const router = useRouter();
const dispatch = useAppDispatch();
const { isFetching, token } = useAppSelector((state) => state.auth);
const { action, businessId } = router.query;
const isClaiming = action === 'claim';
const textColor = useAppSelector((state) => state.style.linkColor);
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const notify = (type, msg) => toast(msg, { type });
const [ illustrationImage, setIllustrationImage ] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('left');
const [showPassword, setShowPassword] = useState(false);
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth,
);
const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com',
password: 'b2096650',
remember: true })
const title = 'Crafted Network'
// Fetch Pexels image/video
useEffect( () => {
async function fetchData() {
const image = await getPexelsImage()
const video = await getPexelsVideo()
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
// Fetch user data
useEffect(() => {
if (token) {
dispatch(findMe());
// If claiming, redirect back to business details after login
if (isClaiming && businessId) {
router.push(`/public/businesses-details?id=${businessId}`);
} else {
router.push('/dashboard');
}
}
}, [token, dispatch, isClaiming, businessId, router]);
}, [token, dispatch]);
// Redirect to dashboard if user is logged in
useEffect(() => {
dispatch(resetAction());
}, [dispatch]);
if (currentUser?.id) {
router.push('/dashboard');
}
}, [currentUser?.id, router]);
// Show error message if there is one
useEffect(() => {
if (errorMessage){
notify('error', errorMessage)
}
const handleSubmit = async (values: any) => {
const { email, password } = values;
const rest = { email, password };
}, [errorMessage])
// Show notification if there is one
useEffect(() => {
if (notifyState?.showNotification) {
notify('success', notifyState?.textNotification)
dispatch(resetAction());
}
}, [notifyState?.showNotification])
const togglePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const handleSubmit = async (value) => {
const {remember, ...rest} = value
await dispatch(loginUser(rest));
};
return (
<SectionFullScreen bg="white">
<Head>
<title>{getPageTitle('Login')} | {title}</title>
</Head>
const setLogin = (target: HTMLElement) => {
setInitialValues(prev => ({
...prev,
email : target.innerText.trim(),
password: target.dataset.password ?? '',
}));
};
<div className="flex flex-col lg:flex-row min-h-screen w-full overflow-hidden">
{/* Left Side: Visual/Branding */}
<div className="hidden lg:flex lg:w-1/2 bg-slate-900 items-center justify-center p-12 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full opacity-20">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-emerald-500 blur-[120px] rounded-full"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-600 blur-[120px] rounded-full"></div>
const imageBlock = (image) => (
<div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3"
style={{
backgroundImage: `${image ? `url(${image.src?.original})` : 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}>
<div className="flex justify-center w-full bg-blue-300/20">
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
by {image?.photographer} on Pexels</a>
</div>
<div className="relative z-10 text-center space-y-8 max-w-md">
<div className="inline-flex p-4 rounded-3xl bg-emerald-500 shadow-2xl shadow-emerald-500/20 rotate-3">
<BaseIcon path={mdiShieldCheck} size={48} className="text-slate-900" />
</div>
<h1 className="text-5xl font-black text-white tracking-tight">
The <span className="text-emerald-400">Crafted</span> Network
</h1>
<p className="text-xl text-slate-400 leading-relaxed font-medium">
Join the world&apos;s most trusted network for verified professional services.
</p>
</div>
</div>
{/* Right Side: Login Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-6 md:p-12 bg-white">
<div className="w-full max-w-md space-y-10 animate-fade-in">
{/* Back Button */}
<Link href="/" className="inline-flex items-center text-sm font-bold text-slate-400 hover:text-emerald-600 transition-colors group">
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2 group-hover:-translate-x-1 transition-transform" />
Back to Home
</Link>
<div className="space-y-4">
<h2 className="text-4xl font-black text-slate-900 tracking-tight">Welcome Back</h2>
{isClaiming ? (
<div className="bg-emerald-50 p-6 rounded-2xl border border-emerald-100 flex items-start space-x-4">
<div className="p-2 bg-emerald-100 rounded-xl text-emerald-600">
<BaseIcon path={mdiShieldCheck} size={24} />
</div>
<div className="space-y-1">
<h4 className="font-bold text-emerald-900">Verify Ownership</h4>
<p className="text-emerald-700 text-sm">Please login or create an account to verify ownership and take control of your business profile.</p>
</div>
</div>
) : (
<p className="text-slate-500">Log in to manage your professional presence and track your requests.</p>
)}
</div>
<CardBox className="border-none shadow-none p-0">
<Formik
initialValues={{ email: '', password: '' }}
validate={validate}
onSubmit={handleSubmit}
>
{({ errors, touched }) => (
<Form className="space-y-6">
<FormField label="Email Address" labelColor="text-slate-900 font-bold" help={touched.email && errors.email ? (errors.email as string) : "Your registered professional email"}>
<Field
name="email"
type="email"
placeholder="alex@example.com"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.email && errors.email ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
<FormField label="Password" labelColor="text-slate-900 font-bold" help={touched.password && errors.password ? (errors.password as string) : "Security first — keep it safe"}>
<Field
name="password"
type="password"
placeholder="••••••••"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.password && errors.password ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
<div className="flex items-center justify-between text-sm font-bold pt-2">
<Link href="/forgot-password" size="sm" className="text-emerald-600 hover:text-emerald-500 transition-colors">
Forgot Password?
</Link>
</div>
<BaseButton
type="submit"
color="emerald"
label={isFetching ? 'Verifying...' : 'Login to Dashboard'}
className="w-full py-5 rounded-2xl font-black text-lg shadow-xl shadow-emerald-500/20 active:scale-[0.98] transition-all"
disabled={isFetching}
/>
</Form>
)}
</Formik>
<div className="pt-10 text-center space-y-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-100"></div>
</div>
<div className="relative flex justify-center text-xs uppercase font-bold tracking-widest text-slate-400">
<span className="bg-white px-4">New to the Network?</span>
</div>
</div>
<Link
href={isClaiming ? `/register?action=claim&businessId=${businessId}` : "/register"}
className="block w-full py-4 px-6 rounded-2xl bg-slate-50 border-2 border-slate-100 text-slate-900 font-bold hover:bg-slate-100 transition-all text-center"
>
Create Professional Account
</Link>
<p className='text-xs font-medium text-slate-400 pt-8'>
© 2026 <span>{title}</span>. All rights reserved. Professional Directory Platform.
</p>
</div>
</CardBox>
</div>
</div>
</div>
</SectionFullScreen>
)
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video.user.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div style={contentPosition === 'background' ? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
} : {}}>
<Head>
<title>{getPageTitle('Login')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox id="loginRoles" className='w-full md:w-3/5 lg:w-2/3'>
<h2 className="text-4xl font-semibold my-4">{title}</h2>
<div className='flex flex-row text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `}
data-password="b2096650"
onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>b2096650</code>{' / '}
to login as Admin</p>
<p>Use <code
className={`cursor-pointer ${textColor} `}
data-password="7302e7d1c0fe"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>7302e7d1c0fe</code>{' / '}
to login as User</p>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w='w-16'
h='h-16'
size={48}
path={mdiInformation}
/>
</div>
</div>
</CardBox>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<Formik
initialValues={initialValues}
enableReinitialize
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
className='absolute bottom-8 right-0 pr-3 flex items-center cursor-pointer'
onClick={togglePasswordVisibility}
>
<BaseIcon
className='text-gray-500 hover:text-gray-700'
size={20}
path={showPassword ? mdiEyeOff : mdiEye}
/>
</div>
</div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
</Link>
</div>
<BaseDivider />
<BaseButtons>
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
color='info'
disabled={isFetching}
/>
</BaseButtons>
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
</Link>
</p>
</Form>
</Formik>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<ToastContainer />
</div>
);
}
LoginPage.getLayout = function getLayout(page: ReactElement) {
return page;
};
Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,73 +1,292 @@
import React from 'react';
import Head from 'next/head';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import LayoutGuest from '../layouts/Guest';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
const PrivacyPolicyPage = () => {
const title = 'Crafted Network';
export default function PrivacyPolicy() {
const title = 'Crafted Network'
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
setProjectUrl(location.origin);
}, []);
const Introduction = () => {
return (
<>
<h3>1. Introduction</h3>
<p>
{/* eslint-disable-next-line react/no-unescaped-entities */}
We at <span>{title}</span> ("we", "us", "our") are committed to
protecting your privacy. This Privacy Policy explains how we collect,
use, disclose, and safeguard your information when you visit our
website <a href={projectUrl}>{projectUrl}</a>, use our services, or
interact with us in other ways. By using our services, you agree to
the collection and use of information in accordance with this policy.
</p>
</>
);
};
const Information = () => {
return (
<>
<h3>2. Information We Collect</h3>
<div className='ml-2'>
<h4>2.1 Personal Identification Information</h4>
<p>
We collect various types of personal information in connection with
the services we provide, including:
</p>
<ul role='list'>
<li>
Contact Information: Name, email address, phone number, mailing
address.
</li>
<li>Account Information: Username, password, profile picture.</li>
<li>Payment Information: Credit card details, billing address.</li>
<li>Demographic Information: Age, gender, interests.</li>
</ul>
<h4>2.2 Technical Data</h4>
<p>
We automatically collect certain information when you visit, use, or
navigate our services. This information may include:
</p>
<ul role='list'>
<li>
Device Information: IP address, browser type, operating system,
device type.
</li>
<li>
Usage Data: Pages visited, time spent on each page, links clicked,
and other actions taken on our site.
</li>
</ul>
<h4>2.3 Cookies and Tracking Technologies</h4>
<p>
We use cookies and similar tracking technologies to track the
activity on our service and hold certain information. You can
instruct your browser to refuse all cookies or to indicate when a
cookie is being sent.
</p>
</div>
</>
);
};
const HowToUser = () => {
return (
<>
<h3>3. How We Use Your Information</h3>
<p>We use the information we collect in various ways, including to:</p>
<ul role='list' className=''>
<li>Provide, operate, and maintain our website and services.</li>
<li>Improve, personalize, and expand our website and services.</li>
<li>Understand and analyze how you use our website and services.</li>
<li>Develop new products, services, features, and functionality.</li>
<li>
Communicate with you, either directly or through one of our
partners, including for customer service, to provide you with
updates and other information relating to the website, and for
marketing and promotional purposes.
</li>
<li>
Process your transactions and send you related information,
including purchase confirmations and invoices.
</li>
<li>Find and prevent fraud.</li>
<li>Comply with legal obligations.</li>
</ul>
</>
);
};
const DataProtection = () => {
return (
<>
<h3>4. Data Protection and Security</h3>
<p>
We implement a variety of security measures to maintain the safety of
your personal information. These measures include:
</p>
<ul role='list'>
<li>
Encryption: We use encryption to protect sensitive information
transmitted online. Access Controls: We restrict access to your
personal data to authorized personnel only. Regular Security Audits:
We conduct regular audits to identify and address potential security
vulnerabilities.
</li>
</ul>
</>
);
};
const Sharing = () => {
return (
<>
<h3>5. Sharing Your Information</h3>
<p>
We do not sell, trade, or otherwise transfer your Personally
Identifiable Information to outside parties without your consent,
except in the following cases:
</p>
<ul role='list'>
<li>
Service Providers: We may share your information with third-party
service providers who perform services on our behalf, such as
payment processing, data analysis, email delivery, hosting services,
customer service, and marketing assistance.
</li>
<li>
Business Transfers: In the event of a merger, acquisition, or sale
of all or a portion of our assets, your information may be
transferred as part of that transaction.
</li>
<li>
Legal Requirements: We may disclose your information if required to
do so by law or in response to valid requests by public authorities
(e.g., a court or a government agency).
</li>
</ul>
</>
);
};
const ProtectionRights = () => {
return (
<>
<h3>6. Your Data Protection Rights</h3>
<p>
Depending on your location, you may have the following rights
regarding your personal data:
</p>
<ul role='list'>
<li>
The Right to Access: You have the right to request copies of your
personal data.
</li>
<li>
The Right to Rectification: You have the right to request that we
correct any information you believe is inaccurate or complete
information you believe is incomplete.
</li>
<li>
The Right to Erasure: You have the right to request that we erase
your personal data, under certain conditions.
</li>
<li>
The Right to Restrict Processing: You have the right to request that
we restrict the processing of your personal data, under certain
conditions.
</li>
<li>
The Right to Object to Processing: You have the right to object to
our processing of your personal data, under certain conditions.
</li>
<li>
The Right to Data Portability: You have the right to request that we
transfer the data that we have collected to another organization, or
directly to you, under certain conditions.
</li>
</ul>
</>
);
};
const DataTransfers = () => {
return (
<>
<h3>7. International Data Transfers</h3>
<p>
Your information, including personal data, may be transferred to and
maintained on computers located outside of your state, province,
country, or other governmental jurisdiction where the data protection
laws may differ from those of your jurisdiction. We will take all
steps reasonably necessary to ensure that your data is treated
securely and in accordance with this Privacy Policy.
</p>
</>
);
};
const RetentionOfData = () => {
return (
<>
<h3>8. Retention of Data</h3>
<p>
We will retain your personal data only for as long as is necessary for
the purposes set out in this Privacy Policy. We will retain and use
your personal data to the extent necessary to comply with our legal
obligations, resolve disputes, and enforce our policies.
</p>
</>
);
};
const ChangePrivacy = () => {
return (
<>
<h3>9. Changes to This Privacy Policy</h3>
<p>
We may update our Privacy Policy from time to time. We will notify you
of any changes by posting the new Privacy Policy on this page. You are
advised to review this Privacy Policy periodically for any changes.
Changes to this Privacy Policy are effective when they are posted on
this page.
</p>
</>
);
};
const ContactUs = () => {
return (
<>
<h3>10. Contact Us</h3>
<p>
If you have any questions about this Privacy Policy, please contact
us:
</p>
<div>
By email:{' '}
<a href='mailto:support@flatlogic.com'> [support@flatlogic.com]</a>
</div>
<div>
By visiting this page on our website:{' '}
<a href='https://flatlogic.com/contact'>Contact Us</a>
</div>
</>
);
};
return (
<>
<div className='prose prose-slate mx-auto max-w-none'>
<Head>
<title>{getPageTitle('Privacy Policy')} | {title}</title>
<title>{getPageTitle('Privacy Policy')}</title>
</Head>
<SectionMain className="py-32">
<div className="max-w-3xl mx-auto space-y-12 animate-fade-in">
<div className="space-y-4">
<h1 className="text-5xl font-black text-slate-900 tracking-tight leading-tight">Privacy Policy</h1>
<p className="text-slate-500 font-medium italic">Last updated: February 17, 2026</p>
</div>
<div className="prose prose-slate max-w-none text-slate-600 space-y-8 font-medium leading-relaxed">
<p className="text-xl text-slate-900 font-bold">
We at <span>{title}</span> (&quot;we&quot;, &quot;us&quot;, &quot;our&quot;) are committed to protecting your privacy and personal data.
</p>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">1. Information We Collect</h2>
<p>
We collect personal information such as your name, email address, and professional details when you register on our platform. We also collect information about your interactions with the service directory to improve our matching algorithm.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">2. How We Use Your Information</h2>
<p>
Your information is used to provide and improve the <span>{title}</span> services, facilitate communication between professionals and clients, and enhance the overall user experience.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">3. Data Sharing and Security</h2>
<p>
We do not sell your personal information to third parties. We use industry-standard security measures to protect your data from unauthorized access or disclosure.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">4. Your Rights</h2>
<p>
You have the right to access, correct, or delete your personal information at any time. You can manage your privacy settings through your professional profile.
</p>
</div>
<div className="space-y-4 border-t border-slate-100 pt-12">
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest">
© 2026 Crafted Network. Built with Trust & Transparency.
</p>
</div>
<div className='flex justify-center'>
<div className='z-10 md:w-10/12 my-4 bg-white border border-pavitra-400 rounded'>
<div className='p-8 lg:px-12 lg:py-10'>
<h1>Privacy Policy</h1>
<Introduction />
<Information />
<HowToUser />
<DataProtection />
<Sharing />
<ProtectionRights />
<DataTransfers />
<RetentionOfData />
<ChangePrivacy />
<ContactUs />
</div>
</div>
</SectionMain>
</>
</div>
</div>
);
};
}
PrivacyPolicyPage.getLayout = function getLayout(page: ReactElement) {
PrivacyPolicy.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default PrivacyPolicyPage;

View File

@ -1,349 +1,372 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Link from 'next/link';
import {
mdiMapMarker,
mdiStar,
mdiShieldCheck,
mdiClockOutline,
mdiMapMarker,
mdiPhone,
mdiEmail,
mdiWeb,
mdiChevronRight,
mdiImageMultiple,
mdiMessageTextOutline,
mdiShareVariant,
mdiFlagOutline,
mdiAccountCheck,
mdiCalendarCheck,
mdiMagnify
mdiEmail,
mdiCurrencyUsd,
mdiCheckDecagram,
mdiMessageDraw,
mdiAccount
} from '@mdi/js';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { fetch as fetchBusiness } from '../../stores/businesses/businessesSlice';
import axios from 'axios';
import LayoutGuest from '../../layouts/Guest';
import SectionMain from '../../components/SectionMain';
import BaseIcon from '../../components/BaseIcon';
import BaseButton from '../../components/BaseButton';
import LoadingSpinner from '../../components/LoadingSpinner';
import { dateFormatter } from '../../helpers/dataFormatter';
import dataFormatter from '../../helpers/dataFormatter';
import { useAppSelector } from '../../stores/hooks';
export default function BusinessDetailsPage() {
const BusinessDetailsPublic = () => {
const router = useRouter();
const { id } = router.query;
const dispatch = useAppDispatch();
const { item: business, isAskingResponse: loading } = useAppSelector((state) => state.businesses);
const [loading, setLoading] = useState(true);
const [business, setBusiness] = useState<any>(null);
const { currentUser } = useAppSelector((state) => state.auth);
const [activeTab, setActiveTab] = useState('about');
useEffect(() => {
if (id && typeof id === 'string') {
dispatch(fetchBusiness({ id }));
if (id) {
fetchBusiness();
}
}, [id, dispatch]);
}, [id]);
const claimListing = () => {
if (!currentUser) {
router.push(`/login?action=claim&businessId=${id}`);
} else {
// In a real app, this would trigger the claim process thunk
alert('Verification process started. Our team will contact you to verify ownership.');
const fetchBusiness = async () => {
setLoading(true);
try {
const response = await axios.get(`/businesses/${id}`);
setBusiness(response.data);
} catch (error) {
console.error('Error fetching business:', error);
} finally {
setLoading(false);
}
};
if (loading || !business) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<LoadingSpinner />
</div>
);
}
const claimListing = async () => {
if (!currentUser) {
router.push('/login');
return;
}
try {
await axios.post(`/businesses/${id}/claim`);
fetchBusiness(); // Refresh data
} catch (error) {
console.error('Error claiming business:', error);
alert('Failed to claim business. Please try again.');
}
};
const isVerified = business.isVerified;
const getBusinessImage = () => {
if (business && business.business_photos_business && business.business_photos_business.length > 0) {
const photo = business.business_photos_business[0].photos && business.business_photos_business[0].photos[0];
if (photo && photo.publicUrl) {
return `/api/file/download?privateUrl=${photo.publicUrl}`;
}
}
return null;
};
if (loading) return <div className="min-h-screen flex items-center justify-center bg-slate-50"><LoadingSpinner /></div>;
if (!business) return <div className="min-h-screen flex items-center justify-center bg-slate-50">Business not found.</div>;
const displayRating = business.rating ? Number(business.rating).toFixed(1) : 'New';
return (
<div className="bg-white min-h-screen font-sans selection:bg-emerald-500/30">
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
<Head>
<title>{business.name} | Crafted Network</title>
</Head>
{/* Hero Header */}
<div className="relative h-[450px] bg-slate-900 overflow-hidden">
{business.business_photos?.[0]?.photo_url ? (
<img
src={business.business_photos[0].photo_url}
alt={business.name}
className="w-full h-full object-cover opacity-60 scale-105"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-slate-900 via-slate-800 to-emerald-900/20" />
)}
<div className="absolute inset-0 bg-gradient-to-t from-white via-transparent to-transparent" />
</div>
<section className="bg-white border-b border-slate-200 pt-16 pb-12">
<div className="container mx-auto px-6">
<div className="flex flex-col lg:flex-row gap-12 items-start">
{/* Business Photo */}
<div className="w-32 h-32 lg:w-48 lg:h-48 bg-slate-100 rounded-[2.5rem] overflow-hidden flex items-center justify-center shadow-inner relative flex-shrink-0">
{getBusinessImage() ? (
<img
src={getBusinessImage()}
alt={business.name}
className="w-full h-full object-cover"
/>
) : (
<BaseIcon path={mdiShieldCheck} size={64} className="text-slate-300" />
)}
{(business.reliability_score >= 80 || business.is_claimed) && (
<div className="absolute -top-2 -right-2 bg-emerald-500 text-white p-2 rounded-full shadow-lg">
<BaseIcon path={mdiCheckDecagram} size={24} />
</div>
)}
</div>
<SectionMain className="-mt-32 relative z-10 pb-32">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Main Info */}
<div className="lg:col-span-2 space-y-12">
<div className="bg-white p-10 rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100 animate-fade-in">
<div className="flex flex-col md:flex-row md:items-start justify-between gap-6 mb-10">
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
{isVerified ? (
<span className="bg-emerald-50 text-emerald-600 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center shadow-sm border border-emerald-100">
<BaseIcon path={mdiShieldCheck} size={16} className="mr-1" />
Verified Business
<div className="flex-grow w-full">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-6">
<div>
<h1 className="text-4xl lg:text-5xl font-bold mb-3">{business.name}</h1>
<div className="flex flex-wrap items-center gap-4 text-slate-500 font-medium">
<span className="flex items-center">
<BaseIcon path={mdiMapMarker} size={18} className="mr-1 text-emerald-500" />
{business.city}, {business.state}
</span>
<span className="flex items-center">
<BaseIcon path={mdiStar} size={18} className="mr-1 text-amber-400" />
{displayRating} Rating
</span>
{business.is_claimed ? (
<span className="bg-emerald-50 text-emerald-600 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider">
Verified Pro
</span>
) : (
<span className="bg-slate-100 text-slate-500 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center border border-slate-200">
Unverified Listing
<span className="bg-slate-100 text-slate-500 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center">
Unclaimed Listing
</span>
)}
<span className="bg-blue-50 text-blue-600 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider border border-blue-100">
{business.business_categories?.[0]?.category?.name || 'Professional'}
</span>
</div>
<h1 className="text-5xl font-black text-slate-900 tracking-tight leading-tight">{business.name}</h1>
<div className="flex items-center text-slate-500 text-lg font-medium">
<BaseIcon path={mdiMapMarker} size={20} className="mr-2 text-emerald-500" />
{business.locations?.[0]?.address || 'Multiple Locations'} {business.locations?.[0]?.city}
</div>
</div>
<div className="flex flex-col items-center p-6 bg-slate-50 rounded-3xl border border-slate-100 shadow-inner">
<div className="flex items-center text-4xl font-black text-slate-900 mb-1">
4.9
<BaseIcon path={mdiStar} size={32} className="text-amber-400 ml-1" />
</div>
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">Trust Score</div>
<div className="flex gap-4">
<button
onClick={() => router.push(`/public/request-service?businessId=${business.id}`)}
className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-4 px-8 rounded-2xl transition-all shadow-xl shadow-emerald-500/20"
>
Request Service
</button>
</div>
</div>
<div className="flex flex-wrap gap-4 pt-8 border-t border-slate-50">
<BaseButton
label="Contact Professional"
color="emerald"
className="px-10 py-4 rounded-2xl font-black shadow-xl shadow-emerald-500/20"
icon={mdiMessageTextOutline}
/>
<BaseButton
label="Request Quote"
color="white"
className="px-10 py-4 rounded-2xl font-black border-2 border-slate-100 hover:bg-slate-50"
icon={mdiCalendarCheck}
href={`/public/request-service?businessId=${business.id}`}
/>
<button className="p-4 rounded-2xl bg-slate-50 text-slate-400 hover:text-emerald-500 transition-colors">
<BaseIcon path={mdiShareVariant} size={24} />
</button>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 py-6 border-t border-slate-100">
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Avg Rating</div>
<div className="text-2xl font-bold text-slate-900">{displayRating} / 5.0</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Response Time</div>
<div className="text-2xl font-bold text-slate-900">~{business.response_time_median_minutes || 30}m</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Status</div>
<div className="flex items-center text-emerald-500 font-bold">
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2 animate-pulse"></div>
Available
</div>
</div>
<div>
<div className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">Total Reviews</div>
<div className="text-2xl font-bold text-slate-900">{business.reviews_business?.length || 0}</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Navigation Tabs */}
<div className="border-b border-slate-100 flex items-center space-x-12 px-2 overflow-x-auto scrollbar-hide">
{['about', 'services', 'reviews', 'photos'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`pb-4 text-sm font-black uppercase tracking-widest transition-all relative ${
activeTab === tab ? 'text-emerald-600' : 'text-slate-400 hover:text-slate-600'
}`}
<div className="container mx-auto px-6 py-12">
<div className="grid lg:grid-cols-3 gap-12">
{/* Main Content */}
<div className="lg:col-span-2 space-y-12">
{!business.is_claimed && (
<div className="bg-amber-50 border border-amber-200 p-8 rounded-[2rem] flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h4 className="text-xl font-bold text-amber-900 mb-2">Is this your business?</h4>
<p className="text-amber-700">Claim your listing to respond to reviews, update your profile, and get more leads.</p>
</div>
<button
onClick={claimListing}
className="bg-amber-500 hover:bg-amber-600 text-white font-bold py-3 px-8 rounded-xl transition-all flex-shrink-0"
>
{tab}
{activeTab === tab && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-emerald-500 rounded-full animate-fade-in" />
)}
Claim Listing
</button>
))}
</div>
</div>
)}
{/* Tab Content */}
<div className="min-h-[400px]">
{activeTab === 'about' && (
<div className="space-y-12 animate-fade-in">
<div className="prose prose-slate max-w-none">
<h3 className="text-2xl font-black text-slate-900 mb-6">About the Business</h3>
<p className="text-lg text-slate-600 leading-relaxed font-medium italic">
{business.description || 'This professional has not provided a detailed description yet.'}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-slate-50 p-8 rounded-3xl border border-slate-100 space-y-4">
<h4 className="font-bold text-slate-900 flex items-center">
<BaseIcon path={mdiClockOutline} size={20} className="mr-2 text-emerald-500" />
Business Hours
</h4>
<div className="space-y-2 text-sm font-medium text-slate-600">
<div className="flex justify-between"><span>Mon - Fri</span><span>09:00 - 18:00</span></div>
<div className="flex justify-between"><span>Saturday</span><span>10:00 - 15:00</span></div>
<div className="flex justify-between font-bold text-emerald-600 italic"><span>Sunday</span><span>Closed</span></div>
</div>
</div>
<div className="bg-slate-50 p-8 rounded-3xl border border-slate-100 space-y-4">
<h4 className="font-bold text-slate-900 flex items-center">
<BaseIcon path={mdiAccountCheck} size={20} className="mr-2 text-emerald-500" />
Verification Status
</h4>
<p className="text-sm font-medium text-slate-500 italic">
{isVerified
? 'This business has verified its identity, ownership, and credentials with the Crafted Network.'
: 'Identity verification is pending for this listing.'}
</p>
</div>
</div>
</div>
)}
{activeTab === 'reviews' && (
<div className="space-y-8 animate-fade-in">
<div className="flex items-center justify-between">
<h3 className="text-2xl font-black text-slate-900">Customer Feedback</h3>
<BaseButton label="Write a Review" color="emerald" small className="rounded-xl px-6" />
</div>
{business.reviews && business.reviews.length > 0 ? (
business.reviews.map((review: any) => (
<div key={review.id} className="p-8 bg-slate-50 rounded-3xl border border-slate-100 space-y-4">
<div className="flex justify-between items-start">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-emerald-100 rounded-full flex items-center justify-center font-bold text-emerald-600 shadow-sm">
{review.user?.firstName?.[0] || 'U'}
</div>
<div>
<div className="font-black text-slate-900">{review.user?.firstName} {review.user?.lastName}</div>
<div className="text-xs text-slate-400 font-medium">{dateFormatter(review.created_at_ts)}</div>
</div>
</div>
<div className="flex text-amber-400">
{[...Array(5)].map((_, i) => (
<BaseIcon key={i} path={mdiStar} size={16} className={i < review.rating ? 'fill-current' : 'text-slate-200'} />
))}
</div>
</div>
<p className="text-slate-600 font-medium leading-relaxed italic">&quot;{review.comment}&quot;</p>
{/* Photos Gallery */}
{business.business_photos_business?.length > 0 && (
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">Photos</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{business.business_photos_business.map((bp: any) => (
bp.photos?.map((p: any) => (
<div key={p.id} className="aspect-square rounded-2xl overflow-hidden bg-slate-100">
<img
src={`/api/file/download?privateUrl=${p.publicUrl}`}
alt="Business"
className="w-full h-full object-cover hover:scale-110 transition-transform duration-500"
/>
</div>
))
) : (
<div className="text-center py-24 bg-slate-50 rounded-[2.5rem] border-2 border-dashed border-slate-200">
<BaseIcon path={mdiStar} size={48} className="mx-auto text-slate-200 mb-6" />
<p className="text-slate-400 font-bold">No reviews yet. Be the first to share your experience!</p>
</div>
)}
</div>
)}
{activeTab === 'photos' && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-6 animate-fade-in">
{business.business_photos?.map((photo: any) => (
<div key={photo.id} className="group relative aspect-square bg-slate-100 rounded-3xl overflow-hidden cursor-pointer">
<img
src={photo.photo_url}
alt="Gallery"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<BaseIcon path={mdiMagnify} size={48} className="text-white" />
</div>
</div>
))}
{(!business.business_photos || business.business_photos.length === 0) && (
<div className="col-span-full text-center py-24 bg-slate-50 rounded-[2.5rem] border-2 border-dashed border-slate-200">
<BaseIcon path={mdiImageMultiple} size={48} className="mx-auto text-slate-200 mb-6" />
<p className="text-slate-400 font-bold">No photos available yet.</p>
</div>
)}
</div>
)}
</div>
</section>
)}
{/* About */}
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">About the Business</h3>
<div className="text-slate-600 leading-relaxed text-lg"
dangerouslySetInnerHTML={{ __html: business.description || 'No description provided.' }} />
</section>
{/* Pricing */}
<section className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-2xl font-bold mb-6">Service Pricing Range</h3>
<div className="grid gap-4">
{business.service_prices_business?.map((price: any) => (
<div key={price.id} className="flex items-center justify-between p-6 rounded-2xl bg-slate-50 hover:bg-emerald-50 transition-colors group">
<div>
<h4 className="font-bold text-slate-800 text-lg group-hover:text-emerald-700">{price.service_name}</h4>
<p className="text-slate-500 text-sm">{price.notes || 'Standard professional service.'}</p>
</div>
<div className="text-right">
<div className="text-emerald-600 font-bold text-xl">${price.typical_price}</div>
<div className="text-xs text-slate-400 font-medium">Typical Price</div>
</div>
</div>
))}
{!business.service_prices_business?.length && <p className="text-slate-500">No pricing information available.</p>}
</div>
</section>
{/* Reviews */}
<section>
<div className="flex items-center justify-between mb-8">
<h3 className="text-2xl font-bold">Customer Reviews</h3>
<button
onClick={() => router.push(`/reviews/reviews-new?businessId=${business.id}`)}
className="flex items-center gap-2 bg-white border border-slate-200 px-6 py-3 rounded-2xl text-emerald-600 font-bold hover:bg-slate-50 transition-all shadow-sm"
>
<BaseIcon path={mdiMessageDraw} size={20} />
Write a Review
</button>
</div>
<div className="grid gap-6">
{business.reviews_business?.map((review: any) => (
<div key={review.id} className="bg-white p-8 rounded-3xl border border-slate-200 shadow-sm hover:shadow-md transition-all">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<BaseIcon key={i} path={mdiStar} size={18} className={i < review.rating ? 'text-amber-400' : 'text-slate-200'} />
))}
</div>
<span className="text-xs text-slate-400 font-medium">{dataFormatter.dateFormatter(review.created_at_ts)}</span>
</div>
<p className="text-slate-700 leading-relaxed mb-4 italic text-lg">&quot;{review.text}&quot;</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center text-slate-400">
<BaseIcon path={mdiAccount} size={18} />
</div>
<span className="text-sm font-bold text-slate-600">
{review.user?.firstName || 'Anonymous'}
</span>
</div>
{review.is_verified_job && (
<div className="inline-flex items-center text-[10px] font-bold text-emerald-600 uppercase tracking-widest bg-emerald-50 px-2 py-1 rounded">
<BaseIcon path={mdiShieldCheck} size={14} className="mr-1" />
Verified Job
</div>
)}
</div>
</div>
))}
{!business.reviews_business?.length && (
<div className="text-center py-20 bg-white rounded-[3rem] border border-dashed border-slate-300 text-slate-400">
<BaseIcon path={mdiMessageDraw} size={48} className="mx-auto mb-4 opacity-20" />
<p className="text-xl font-medium">No reviews yet.</p>
<p className="mb-6">Be the first to share your experience!</p>
<button
onClick={() => router.push(`/reviews/reviews-new?businessId=${business.id}`)}
className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-3 px-8 rounded-xl"
>
Write Review
</button>
</div>
)}
</div>
</section>
</div>
{/* Sidebar */}
<div className="space-y-8">
{/* Contact Card */}
<div className="bg-slate-900 p-8 rounded-[2.5rem] text-white space-y-8 shadow-2xl shadow-slate-300">
<h4 className="text-xl font-black">Contact Information</h4>
<div className="space-y-6">
<a href={`tel:${business.phoneNumber}`} className="flex items-center group">
<div className="w-12 h-12 bg-white/10 rounded-2xl flex items-center justify-center mr-4 group-hover:bg-emerald-500 group-hover:text-slate-900 transition-all">
<BaseIcon path={mdiPhone} size={20} />
</div>
<div className="space-y-0.5">
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">Phone</div>
<div className="font-bold">{business.phoneNumber}</div>
</div>
</a>
<a href={`mailto:${business.email}`} className="flex items-center group">
<div className="w-12 h-12 bg-white/10 rounded-2xl flex items-center justify-center mr-4 group-hover:bg-emerald-500 group-hover:text-slate-900 transition-all">
<BaseIcon path={mdiEmail} size={20} />
</div>
<div className="space-y-0.5">
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">Email</div>
<div className="font-bold truncate max-w-[180px]">{business.email}</div>
</div>
</a>
{business.website && (
<a href={business.website} target="_blank" rel="noopener noreferrer" className="flex items-center group">
<div className="w-12 h-12 bg-white/10 rounded-2xl flex items-center justify-center mr-4 group-hover:bg-emerald-500 group-hover:text-slate-900 transition-all">
<BaseIcon path={mdiWeb} size={20} />
{/* Contact Info */}
<div className="bg-slate-900 text-white p-10 rounded-[3rem] shadow-xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full -mr-16 -mt-16 group-hover:scale-110 transition-transform"></div>
<h3 className="text-xl font-bold mb-8">Contact & Location</h3>
<div className="space-y-6 relative z-10">
<div className="flex items-start">
<BaseIcon path={mdiPhone} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Call Now</div>
<div className="font-bold">{business.phone || 'Contact for details'}</div>
</div>
<div className="space-y-0.5">
<div className="text-xs font-bold text-slate-400 uppercase tracking-widest">Website</div>
<div className="font-bold truncate max-w-[180px]">{business.website.replace(/^https?:\/\/(www\.)?/, '')}</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiEmail} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Email</div>
<div className="font-bold truncate max-w-[180px]">{business.email}</div>
</div>
</a>
)}
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiWeb} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Website</div>
<div className="font-bold truncate max-w-[180px]">{business.website || 'N/A'}</div>
</div>
</div>
<div className="flex items-start">
<BaseIcon path={mdiMapMarker} size={24} className="mr-4 text-emerald-400" />
<div>
<div className="text-xs font-black uppercase tracking-widest text-slate-500 mb-1">Address</div>
<div className="font-bold">{business.address}, {business.city}, {business.state} {business.zip}</div>
</div>
</div>
</div>
</div>
<div className="pt-8 border-t border-white/10 flex items-center justify-between">
<button className="flex items-center text-sm font-bold text-slate-400 hover:text-white transition-colors">
<BaseIcon path={mdiFlagOutline} size={16} className="mr-2" />
Report Problem
</button>
<div className="flex gap-2">
<div className="w-8 h-8 rounded-lg bg-white/5 flex items-center justify-center hover:bg-white/10 transition-colors cursor-pointer">
<BaseIcon path={mdiStar} size={16} />
</div>
{/* Badges */}
<div className="bg-white p-10 rounded-[3rem] border border-slate-200 shadow-sm">
<h3 className="text-xl font-bold mb-8">Trust Signals</h3>
<div className="space-y-6">
{business.business_badges_business?.filter((b:any) => b.status === 'APPROVED').map((badge: any) => (
<div key={badge.id} className="flex items-center p-4 rounded-2xl bg-slate-50">
<div className="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center mr-4 text-emerald-600">
<BaseIcon path={mdiShieldCheck} size={24} />
</div>
<div>
<div className="font-bold text-slate-800 text-sm leading-tight">{badge.badge_type.replace(/_/g, ' ')}</div>
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Verified Badge</div>
</div>
</div>
))}
{business.is_claimed && (
<div className="flex items-center p-4 rounded-2xl bg-emerald-50">
<div className="w-10 h-10 bg-emerald-200 rounded-xl flex items-center justify-center mr-4 text-emerald-700">
<BaseIcon path={mdiCheckDecagram} size={24} />
</div>
<div>
<div className="font-bold text-emerald-900 text-sm leading-tight">Claimed Listing</div>
<div className="text-[10px] text-emerald-600 font-bold uppercase tracking-widest">Verified Owner</div>
</div>
</div>
)}
{!business.business_badges_business?.length && !business.is_claimed && <p className="text-slate-400 text-sm italic">Pending verification...</p>}
</div>
</div>
</div>
{/* Claim Listing Section */}
{!isVerified && (
<div className="bg-emerald-50 p-10 rounded-[2.5rem] border border-emerald-100 text-center space-y-6 relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:scale-150 transition-transform duration-700">
<BaseIcon path={mdiShieldCheck} size={144} className="text-emerald-900" />
</div>
<div className="relative z-10 space-y-6">
<div className="w-20 h-20 bg-emerald-500 rounded-3xl flex items-center justify-center mx-auto shadow-xl shadow-emerald-500/30">
<BaseIcon path={mdiShieldCheck} size={48} className="text-slate-900" />
</div>
<div className="space-y-2">
<h4 className="text-2xl font-black text-emerald-900 tracking-tight">Own this business?</h4>
<p className="text-emerald-700 font-medium leading-relaxed">
Claim this listing to verify your identity, update information, and start receiving leads.
</p>
</div>
<BaseButton
label="Claim Listing"
color="emerald"
className="w-full py-5 rounded-2xl font-black shadow-lg shadow-emerald-500/20 active:scale-95 transition-all"
onClick={claimListing}
/>
<p className="text-xs font-bold text-emerald-600/60 uppercase tracking-widest">Verification Required</p>
</div>
</div>
)}
</div>
</div>
</div>
</SectionMain>
</div>
</div>
);
}
};
BusinessDetailsPage.getLayout = function getLayout(page: ReactElement) {
BusinessDetailsPublic.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};
export default BusinessDetailsPublic;

View File

@ -1,234 +1,202 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik';
import {
mdiCalendarCheck,
mdiAccount,
mdiEmail,
mdiPhone,
mdiShieldCheck,
mdiClockOutline,
mdiMapMarker,
mdiMessageTextOutline,
mdiArrowLeft,
mdiShieldCheck,
mdiLightningBolt
mdiEmail,
mdiAccount,
mdiPhone,
mdiAlertDecagram
} from '@mdi/js';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { fetch as fetchBusiness } from '../../stores/businesses/businessesSlice';
import { create as createLead } from '../../stores/leads/leadsSlice';
import LayoutGuest from '../../layouts/Guest';
import SectionMain from '../../components/SectionMain';
import { Formik, Form, Field } from 'formik';
import axios from 'axios';
import LayoutAuthenticated from '../../layouts/Authenticated';
import BaseIcon from '../../components/BaseIcon';
import LoadingSpinner from '../../components/LoadingSpinner';
import FormField from '../../components/FormField';
import BaseButton from '../../components/BaseButton';
import LoadingSpinner from '../../components/LoadingSpinner';
import BaseIcon from '../../components/BaseIcon';
import Link from 'next/link';
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
import { create as createLead } from '../../stores/leads/leadsSlice';
const validate = (values: any) => {
const errors: any = {};
if (!values.customerName) {
errors.customerName = 'Required';
}
if (!values.customerEmail) {
errors.customerEmail = 'Required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.customerEmail)) {
errors.customerEmail = 'Invalid email address';
}
if (!values.customerPhone) {
errors.customerPhone = 'Required';
}
if (!values.serviceType) {
errors.serviceType = 'Required';
}
if (!values.description) {
errors.description = 'Required';
} else if (values.description.length < 20) {
errors.description = 'Please provide more details (min 20 characters)';
}
return errors;
};
export default function RequestServicePage() {
const RequestServicePage = () => {
const router = useRouter();
const { businessId } = router.query;
const dispatch = useAppDispatch();
const { item: business, isAskingResponse: loadingBiz } = useAppSelector((state) => state.businesses);
const [submitted, setSubmitted] = useState(false);
const { businessId } = router.query;
const [business, setBusiness] = useState<any>(null);
const [loading, setLoading] = useState(false);
const { currentUser } = useAppSelector(state => state.auth);
useEffect(() => {
if (businessId && typeof businessId === 'string') {
dispatch(fetchBusiness({ id: businessId }));
if (businessId) {
fetchBusiness();
}
}, [businessId, dispatch]);
}, [businessId]);
const handleSubmit = async (values: any) => {
const payload = {
...values,
businessId,
status: 'pending'
};
await dispatch(createLead(payload));
setSubmitted(true);
const fetchBusiness = async () => {
try {
const response = await axios.get(`/businesses/${businessId}`);
setBusiness(response.data);
} catch (error) {
console.error('Error fetching business:', error);
}
};
if (loadingBiz || !business) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<LoadingSpinner />
</div>
);
}
const handleSubmit = async (values: any) => {
setLoading(true);
try {
const payload = {
...values,
businessId,
user: currentUser?.id
};
await dispatch(createLead(payload));
router.push('/leads/leads-list'); // Redirect to their leads tracker
} catch (error) {
console.error('Lead creation error:', error);
} finally {
setLoading(false);
}
};
if (submitted) {
return (
<SectionMain className="py-32 flex items-center justify-center min-h-[80vh]">
<div className="max-w-xl w-full text-center space-y-8 p-12 bg-white rounded-[3rem] shadow-2xl shadow-emerald-500/10 border border-emerald-50 animate-fade-in">
<div className="w-24 h-24 bg-emerald-500 rounded-3xl flex items-center justify-center mx-auto shadow-xl shadow-emerald-500/30">
<BaseIcon path={mdiShieldCheck} size={48} className="text-slate-900" />
</div>
<div className="space-y-4">
<h2 className="text-4xl font-black text-slate-900 tracking-tight">Request Sent!</h2>
<p className="text-lg text-slate-500 leading-relaxed font-medium">
Your request has been successfully delivered to <span className="text-emerald-600 font-bold">{business.name}</span>. They will review your details and contact you shortly.
</p>
</div>
<div className="pt-8">
<Link href="/" className="bg-slate-900 text-white font-black px-12 py-5 rounded-2xl hover:bg-slate-800 transition-all shadow-xl shadow-slate-900/20 active:scale-95 inline-block">
Back to Marketplace
</Link>
</div>
</div>
</SectionMain>
);
}
if (!business && businessId) return <div className="min-h-screen flex items-center justify-center bg-slate-50"><LoadingSpinner /></div>;
return (
<div className="bg-slate-50 min-h-screen font-sans selection:bg-emerald-500/30">
<div className="min-h-screen bg-slate-50 pb-20 pt-20">
<Head>
<title>Request Service | Crafted Network</title>
</Head>
<SectionMain className="py-24">
<div className="max-w-6xl mx-auto flex flex-col lg:flex-row gap-12">
{/* Form Side */}
<div className="flex-1 space-y-12 animate-fade-in">
<div className="space-y-6">
<Link href={`/public/businesses-details?id=${business.id}`} className="inline-flex items-center text-sm font-bold text-slate-400 hover:text-emerald-600 transition-colors group">
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2 group-hover:-translate-x-1 transition-transform" />
Back to Profile
</Link>
<h1 className="text-5xl font-black text-slate-900 tracking-tight leading-tight">Request <span className="text-emerald-500">Service</span></h1>
<p className="text-lg text-slate-500 font-medium leading-relaxed max-w-lg">
You are requesting a service from <span className="text-emerald-400 font-bold">{business?.name || 'a professional'}</span>. Your details are protected by our verified network.
</p>
<div className="container mx-auto px-6 max-w-4xl">
<div className="bg-white rounded-[3rem] shadow-xl border border-slate-200 overflow-hidden">
<div className="bg-slate-900 p-12 text-white relative">
<div className="absolute top-0 right-0 p-12 opacity-10">
<BaseIcon path={mdiShieldCheck} size={120} />
</div>
<h1 className="text-4xl font-bold mb-4">Request Service</h1>
<p className="text-slate-400 text-lg max-w-xl">
You are requesting a service from <span className="text-emerald-400 font-bold">{business?.name || 'a professional'}</span>.
Our smart matching system ensures your request is handled with priority.
</p>
</div>
<div className="bg-white p-10 md:p-12 rounded-[2.5rem] shadow-xl shadow-slate-200/50 border border-white">
<Formik
initialValues={{
customerName: '',
customerEmail: '',
customerPhone: '',
serviceType: '',
description: ''
}}
validate={validate}
onSubmit={handleSubmit}
>
{({ errors, touched }) => (
<Form className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<FormField label="Full Name" labelColor="text-slate-900 font-bold" help={touched.customerName && errors.customerName ? (errors.customerName as string) : ""}>
<Field name="customerName" placeholder="John Doe" className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.customerName && errors.customerName ? 'border-red-500' : 'border-slate-50'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`} />
</FormField>
<FormField label="Email Address" labelColor="text-slate-900 font-bold" help={touched.customerEmail && errors.customerEmail ? (errors.customerEmail as string) : ""}>
<Field name="customerEmail" type="email" placeholder="john@example.com" className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.customerEmail && errors.customerEmail ? 'border-red-500' : 'border-slate-50'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`} />
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<FormField label="Phone Number" labelColor="text-slate-900 font-bold" help={touched.customerPhone && errors.customerPhone ? (errors.customerPhone as string) : ""}>
<Field name="customerPhone" placeholder="+1 (555) 000-0000" className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.customerPhone && errors.customerPhone ? 'border-red-500' : 'border-slate-50'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`} />
</FormField>
<FormField label="Service Type" labelColor="text-slate-900 font-bold" help={touched.serviceType && errors.serviceType ? (errors.serviceType as string) : ""}>
<Field name="serviceType" placeholder="Consultation, Repair, etc." className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.serviceType && errors.serviceType ? 'border-red-500' : 'border-slate-50'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`} />
</FormField>
</div>
<FormField label="Request Details" labelColor="text-slate-900 font-bold" help={touched.description && errors.description ? (errors.description as string) : "Explain what you need in detail for a better quote."}>
<Field
name="description"
as="textarea"
rows={6}
placeholder="Please describe the service you require..."
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.description && errors.description ? 'border-red-500' : 'border-slate-50'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900 resize-none`}
/>
<div className="p-12">
<Formik
initialValues={{
keyword: '',
description: '',
urgency: 'TODAY',
contact_name: currentUser ? `${currentUser.firstName} ${currentUser.lastName}` : '',
contact_email: currentUser?.email || '',
contact_phone: currentUser?.phoneNumber || '',
address: '',
city: '',
state: '',
zip: ''
}}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form className="space-y-8">
<div className="grid md:grid-cols-2 gap-8">
<FormField label="What do you need help with?" labelFor="keyword">
<Field
name="keyword"
placeholder="e.g. Leaking faucet in kitchen"
className="w-full bg-slate-50 border-slate-200 rounded-2xl py-4 px-6 focus:ring-emerald-500 focus:border-emerald-500"
/>
</FormField>
<div className="pt-4">
<BaseButton
type="submit"
color="emerald"
label="Send Request"
className="w-full py-5 rounded-2xl font-black text-xl shadow-xl shadow-emerald-500/20 active:scale-[0.98] transition-all"
icon={mdiLightningBolt}
/>
</div>
</Form>
)}
</Formik>
</div>
</div>
<FormField label="Urgency" labelFor="urgency">
<Field
name="urgency"
as="select"
className="w-full bg-slate-50 border-slate-200 rounded-2xl py-4 px-6 focus:ring-emerald-500 focus:border-emerald-500"
>
<option value="EMERGENCY">🚨 Emergency (Immediate)</option>
<option value="TODAY">📅 Today</option>
<option value="THIS_WEEK">🗓 This Week</option>
<option value="FLEXIBLE">🍃 Flexible</option>
</Field>
</FormField>
</div>
{/* Info Side */}
<div className="w-full lg:w-96 space-y-8 lg:mt-32">
<div className="bg-slate-900 p-8 rounded-[2.5rem] text-white space-y-8 shadow-2xl shadow-slate-300">
<div className="space-y-4">
<h4 className="text-xl font-black">Trust Guarantee</h4>
<p className="text-slate-400 text-sm font-medium leading-relaxed">
Your information is only shared with the selected professional. We never sell your data or send spam.
</p>
</div>
<FormField label="Details of the issue" labelFor="description">
<Field
name="description"
as="textarea"
rows={4}
placeholder="Please describe the problem in detail so the professional can give you an accurate estimate."
className="w-full bg-slate-50 border-slate-200 rounded-2xl py-4 px-6 focus:ring-emerald-500 focus:border-emerald-500"
/>
</FormField>
<div className="space-y-6">
{[
{ icon: mdiShieldCheck, label: 'Verified Professional', desc: 'Identity and credentials checked.' },
{ icon: mdiLightningBolt, label: 'Fast Response', desc: 'Avg. response time under 2 hours.' },
{ icon: mdiMessageTextOutline, label: 'Direct Chat', desc: 'Communicate securely on platform.' }
].map((item, i) => (
<div key={i} className="flex items-start space-x-4">
<div className="p-2 bg-emerald-500 rounded-xl text-slate-900">
<BaseIcon path={item.icon} size={20} />
<div className="bg-slate-50 p-8 rounded-3xl space-y-8">
<h3 className="text-xl font-bold flex items-center">
<BaseIcon path={mdiAccount} size={24} className="mr-3 text-emerald-500" />
Contact Information
</h3>
<div className="grid md:grid-cols-3 gap-6">
<FormField label="Your Name" labelFor="contact_name">
<Field name="contact_name" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
<FormField label="Email" labelFor="contact_email">
<Field name="contact_email" type="email" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
<FormField label="Phone" labelFor="contact_phone">
<Field name="contact_phone" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
</div>
<div>
<div className="font-bold text-sm">{item.label}</div>
<div className="text-xs text-slate-500">{item.desc}</div>
<div className="grid md:grid-cols-2 gap-6">
<FormField label="Service Address" labelFor="address">
<Field name="address" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
<div className="grid grid-cols-3 gap-4">
<FormField label="City" labelFor="city">
<Field name="city" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
<FormField label="State" labelFor="state">
<Field name="state" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
<FormField label="ZIP" labelFor="zip">
<Field name="zip" className="w-full bg-white border-slate-200 rounded-xl py-3 px-4" />
</FormField>
</div>
</div>
</div>
))}
</div>
</div>
<div className="bg-white p-8 rounded-[2.5rem] border border-slate-200 flex items-center space-x-4">
<div className="w-16 h-16 bg-slate-100 rounded-2xl overflow-hidden shadow-inner">
{business.business_photos?.[0]?.photo_url && (
<img src={business.business_photos[0].photo_url} alt={business.name} className="w-full h-full object-cover" />
)}
</div>
<div className="flex-1">
<div className="text-xs font-black text-slate-400 uppercase tracking-widest mb-1">Requesting from</div>
<div className="font-black text-slate-900 truncate">{business.name}</div>
</div>
</div>
<div className="flex items-center justify-between pt-8 border-t border-slate-100">
<div className="text-slate-500 text-sm flex items-center max-w-sm">
<BaseIcon path={mdiShieldCheck} size={20} className="mr-2 text-emerald-500" />
Your data is protected and will only be shared with the professional you request.
</div>
<BaseButton
type="submit"
color="emerald"
label={loading ? 'Submitting...' : 'Send Request'}
className="py-5 px-12 rounded-2xl text-lg font-bold shadow-2xl shadow-emerald-500/30"
disabled={loading}
/>
</div>
</Form>
)}
</Formik>
</div>
</div>
</SectionMain>
</div>
</div>
);
}
};
RequestServicePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
return (
<LayoutAuthenticated permission={'CREATE_LEADS'}>
{page}
</LayoutAuthenticated>
);
};
export default RequestServicePage;

View File

@ -1,205 +1,92 @@
import React, { useEffect } from 'react';
import React from 'react';
import type { ReactElement } from 'react';
import { ToastContainer, toast } from 'react-toastify';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Formik, Form, Field } from 'formik';
import { mdiAccount, mdiEmail, mdiAsterisk, mdiShieldCheck, mdiArrowLeft } from '@mdi/js';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik';
import FormField from '../components/FormField';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { registerUser, resetAction } from '../stores/authSlice';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router';
import { getPageTitle } from '../config';
const validate = (values: any) => {
const errors: any = {};
if (!values.firstName) {
errors.firstName = 'Required';
}
if (!values.lastName) {
errors.lastName = 'Required';
}
if (!values.email) {
errors.email = 'Required';
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 8) {
errors.password = 'Must be at least 8 characters';
}
return errors;
};
import axios from "axios";
export default function RegisterPage() {
const router = useRouter();
const dispatch = useAppDispatch();
const { isFetching, token } = useAppSelector((state) => state.auth);
const { action, businessId } = router.query;
const isClaiming = action === 'claim';
export default function Register() {
const [loading, setLoading] = React.useState(false);
const router = useRouter();
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const title = 'Crafted Network'
useEffect(() => {
if (token) {
if (isClaiming && businessId) {
router.push(`/login?action=claim&businessId=${businessId}`);
} else {
router.push('/dashboard');
}
}
}, [token, isClaiming, businessId, router]);
const handleSubmit = async (value) => {
setLoading(true)
try {
const { data: response } = await axios.post('/auth/signup',value);
await router.push('/login')
setLoading(false)
notify('success', 'Please check your email for verification link')
} catch (error) {
setLoading(false)
console.log('error: ', error)
notify('error', 'Something was wrong. Try again')
}
};
useEffect(() => {
dispatch(resetAction());
}, [dispatch]);
return (
<>
<Head>
<title>{getPageTitle('Login')}</title>
</Head>
const handleSubmit = async (values: any) => {
await dispatch(registerUser(values));
};
<SectionFullScreen bg='violet'>
<CardBox className='w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12'>
<Formik
initialValues={{
email: '',
password: '',
confirm: ''
}}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Email' help='Please enter your email'>
<Field type='email' name='email' />
</FormField>
<FormField label='Password' help='Please enter your password'>
<Field type='password' name='password' />
</FormField>
<FormField label='Confirm Password' help='Please confirm your password'>
<Field type='password' name='confirm' />
</FormField>
return (
<SectionFullScreen bg="white">
<Head>
<title>{getPageTitle('Register')} | {title}</title>
</Head>
<BaseDivider />
<div className="flex flex-col lg:flex-row min-h-screen w-full overflow-hidden">
{/* Left Side: Visual/Branding */}
<div className="hidden lg:flex lg:w-1/2 bg-slate-900 items-center justify-center p-12 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full opacity-20">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-emerald-500 blur-[120px] rounded-full"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-600 blur-[120px] rounded-full"></div>
</div>
<div className="relative z-10 text-center space-y-8 max-w-md">
<div className="inline-flex p-4 rounded-3xl bg-emerald-500 shadow-2xl shadow-emerald-500/20 -rotate-3">
<BaseIcon path={mdiShieldCheck} size={48} className="text-slate-900" />
</div>
<h1 className="text-5xl font-black text-white tracking-tight">
Join the <span className="text-emerald-400">Crafted</span> Network
</h1>
<p className="text-xl text-slate-400 leading-relaxed font-medium">
Join thousands of service professionals who trust our platform for verified leads and transparent tools.
</p>
</div>
</div>
{/* Right Side: Register Form */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-6 md:p-12 bg-white">
<div className="w-full max-w-md space-y-10 animate-fade-in">
{/* Back Button */}
<Link href="/" className="inline-flex items-center text-sm font-bold text-slate-400 hover:text-emerald-600 transition-colors group">
<BaseIcon path={mdiArrowLeft} size={20} className="mr-2 group-hover:-translate-x-1 transition-transform" />
Back to Home
</Link>
<div className="space-y-4">
<h2 className="text-4xl font-black text-slate-900 tracking-tight">Get Started</h2>
{isClaiming ? (
<div className="bg-emerald-50 p-6 rounded-2xl border border-emerald-100 flex items-start space-x-4">
<div className="p-2 bg-emerald-100 rounded-xl text-emerald-600">
<BaseIcon path={mdiShieldCheck} size={24} />
</div>
<div className="space-y-1">
<h4 className="font-bold text-emerald-900">Step 1: Create Account</h4>
<p className="text-emerald-700 text-sm">Create your professional profile to verify and take control of your business listing.</p>
</div>
</div>
) : (
<p className="text-slate-500">Create your professional profile in the global service directory.</p>
)}
</div>
<CardBox className="border-none shadow-none p-0">
<Formik
initialValues={{ firstName: '', lastName: '', email: '', password: '' }}
validate={validate}
onSubmit={handleSubmit}
>
{({ errors, touched }) => (
<Form className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<FormField label="First Name" labelColor="text-slate-900 font-bold" help={touched.firstName && errors.firstName ? (errors.firstName as string) : ""}>
<Field
name="firstName"
placeholder="John"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.firstName && errors.firstName ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
<FormField label="Last Name" labelColor="text-slate-900 font-bold" help={touched.lastName && errors.lastName ? (errors.lastName as string) : ""}>
<Field
name="lastName"
placeholder="Doe"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.lastName && errors.lastName ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
</div>
<FormField label="Email Address" labelColor="text-slate-900 font-bold" help={touched.email && errors.email ? (errors.email as string) : ""}>
<Field
name="email"
type="email"
placeholder="john@example.com"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.email && errors.email ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
<FormField label="Password" labelColor="text-slate-900 font-bold" help={touched.password && errors.password ? (errors.password as string) : "Minimum 8 characters"}>
<Field
name="password"
type="password"
placeholder="••••••••"
className={`w-full px-5 py-4 bg-slate-50 border-2 ${touched.password && errors.password ? 'border-red-500' : 'border-slate-100'} rounded-2xl focus:border-emerald-500 focus:ring-0 transition-all font-medium text-slate-900`}
/>
</FormField>
<BaseButton
type="submit"
color="emerald"
label={isFetching ? 'Creating Account...' : 'Join the Network'}
className="w-full py-5 rounded-2xl font-black text-lg shadow-xl shadow-emerald-500/20 active:scale-[0.98] transition-all"
disabled={isFetching}
/>
</Form>
)}
</Formik>
<div className="pt-10 text-center space-y-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-100"></div>
</div>
<div className="relative flex justify-center text-xs uppercase font-bold tracking-widest text-slate-400">
<span className="bg-white px-4">Already a Member?</span>
</div>
</div>
<Link
href={isClaiming ? `/login?action=claim&businessId=${businessId}` : "/login"}
className="block w-full py-4 px-6 rounded-2xl bg-slate-50 border-2 border-slate-100 text-slate-900 font-bold hover:bg-slate-100 transition-all text-center"
>
Log in to your Account
</Link>
<p className='text-xs font-medium text-slate-400 pt-8'>
© 2026 <span>{title}</span>. All rights reserved. Professional Directory Platform.
</p>
</div>
</CardBox>
</div>
</div>
</div>
</SectionFullScreen>
);
<BaseButtons>
<BaseButton
type='submit'
label={loading ? 'Loading...' : 'Register' }
color='info'
/>
<BaseButton
href={'/login'}
label={'Login'}
color='info'
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionFullScreen>
<ToastContainer />
</>
);
}
RegisterPage.getLayout = function getLayout(page: ReactElement) {
return page;
};
Register.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,259 +1,243 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import {
mdiMagnify,
mdiMapMarker,
mdiFilterVariant,
mdiStar,
mdiShieldCheck,
mdiChevronRight,
mdiSortVariant
mdiShieldCheck,
mdiClockOutline,
mdiCurrencyUsd,
mdiFilterVariant
} from '@mdi/js';
import BaseIcon from '../components/BaseIcon';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { fetch as fetchBusinesses } from '../stores/businesses/businessesSlice';
import { fetch as fetchCategories } from '../stores/categories/categoriesSlice';
import axios from 'axios';
import LayoutGuest from '../layouts/Guest';
import SectionMain from '../components/SectionMain';
import BaseIcon from '../components/BaseIcon';
import LoadingSpinner from '../components/LoadingSpinner';
import Link from 'next/link';
import type { ReactElement } from 'react';
const SearchPage = () => {
const SearchView = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { businesses, loading } = useAppSelector((state) => state.businesses);
const { categories } = useAppSelector((state) => state.categories);
const [searchQuery, setSearchQuery] = useState('');
const [locationQuery, setLocationQuery] = useState('');
const executeSearch = useCallback((q: string, l: string, category?: string) => {
let queryStr = '?';
if (q) queryStr += `name=${encodeURIComponent(q)}&`;
if (l) queryStr += `city=${encodeURIComponent(l)}&`;
if (category) queryStr += `category=${encodeURIComponent(category)}&`;
dispatch(fetchBusinesses({ query: queryStr }));
}, [dispatch]);
const { query: searchQueryParam, location: locationParam } = router.query;
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState([]);
const [searchQuery, setSearchQuery] = useState(searchQueryParam || '');
const [location, setLocation] = useState(locationParam || '');
useEffect(() => {
if (router.isReady) {
const q = router.query.q as string || '';
const l = router.query.l as string || '';
const category = router.query.category as string || '';
setSearchQuery(q);
setLocationQuery(l);
executeSearch(q, l, category);
dispatch(fetchCategories({ query: '' }));
if (searchQueryParam) {
setSearchQuery(searchQueryParam as string);
fetchData(searchQueryParam as string);
}
}, [dispatch, router.isReady, router.query, executeSearch]);
}, [searchQueryParam]);
const handleSearchClick = () => {
const fetchData = async (query: string) => {
setLoading(true);
try {
const response = await axios.post('/search', { searchQuery: query });
setSearchResults(response.data);
} catch (error) {
console.error('Search error:', error);
} finally {
setLoading(false);
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
router.push({
pathname: '/search',
query: { q: searchQuery, l: locationQuery },
query: { query: searchQuery, location },
});
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearchClick();
const businesses = searchResults.filter((item: any) => item.tableName === 'businesses');
const getBusinessImage = (biz: any) => {
if (biz.business_photos_business && biz.business_photos_business.length > 0) {
const photo = biz.business_photos_business[0].photos && biz.business_photos_business[0].photos[0];
if (photo && photo.publicUrl) {
return `/api/file/download?privateUrl=${photo.publicUrl}`;
}
}
return null;
};
return (
<div className="bg-white min-h-screen font-sans selection:bg-emerald-500/30">
<div className="min-h-screen bg-slate-50 pb-20">
<Head>
<title>Find Services | Crafted Network</title>
</Head>
{/* Hero Search Area */}
<section className="bg-slate-900 pt-32 pb-24 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full opacity-10">
<div className="absolute -top-1/2 -left-1/4 w-full h-full bg-emerald-500 blur-[150px] rounded-full"></div>
</div>
<SectionMain className="relative z-10">
<div className="max-w-5xl mx-auto space-y-12">
<h1 className="text-4xl md:text-5xl font-black text-white text-center tracking-tight leading-tight">
What <span className="text-emerald-400">service</span> are you looking for today?
</h1>
<div className="flex flex-col md:flex-row gap-4 bg-slate-800/50 p-2 rounded-2xl border border-slate-700/50 backdrop-blur-xl shadow-2xl">
<div className="flex-1 relative group">
<BaseIcon path={mdiMagnify} size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Service, professional or business name"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
<div className="w-px bg-slate-700 hidden md:block"></div>
<div className="flex-1 relative group">
<BaseIcon path={mdiMapMarker} size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 group-focus-within:text-emerald-400" />
<input
type="text"
value={locationQuery}
onChange={(e) => setLocationQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="City or Zip code"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
<button
onClick={handleSearchClick}
className="bg-emerald-500 hover:bg-emerald-400 text-slate-900 font-black px-10 py-4 rounded-xl transition-all shadow-lg shadow-emerald-500/20 active:scale-95"
>
Search
</button>
{/* Search Header */}
<div className="bg-slate-900 pt-32 pb-12 shadow-inner">
<div className="container mx-auto px-6">
<form onSubmit={handleSearch} className="flex flex-col md:flex-row gap-4 p-2 bg-white/10 backdrop-blur-md rounded-2xl border border-white/10 shadow-xl max-w-4xl mx-auto">
<div className="flex-grow relative">
<BaseIcon path={mdiMagnify} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Service (e.g. Plumbing)"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
</div>
</SectionMain>
</section>
<div className="md:w-px h-8 bg-white/10 my-auto"></div>
<div className="flex-grow relative">
<BaseIcon path={mdiMapMarker} size={24} className="absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400" />
<input
type="text"
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="Location"
className="w-full bg-transparent border-none focus:ring-0 py-3 pl-12 pr-4 text-white placeholder-slate-500"
/>
</div>
<button type="submit" className="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-3 px-8 rounded-xl transition-all">
Update Search
</button>
</form>
</div>
</div>
{/* Main Content */}
<SectionMain className="py-16">
<div className="flex flex-col lg:flex-row gap-12">
<div className="container mx-auto px-6 mt-12">
<div className="flex flex-col lg:flex-row gap-8">
{/* Filters Sidebar */}
<aside className="w-full lg:w-64 space-y-8">
<div className="flex items-center justify-between lg:mb-8">
<h3 className="text-xl font-black text-slate-900 flex items-center">
<BaseIcon path={mdiFilterVariant} size={20} className="mr-2" />
Filters
</h3>
<button
onClick={() => {
setSearchQuery('');
setLocationQuery('');
router.push('/search');
}}
className="text-sm font-bold text-emerald-600 hover:text-emerald-500"
>
Reset
</button>
</div>
<div className="space-y-6">
<div className="space-y-4">
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-widest">Categories</h4>
<div className="space-y-2">
{categories && categories.slice(0, 8).map((cat: any) => (
<Link
key={cat.id}
href={`/search?category=${cat.id}`}
className="flex items-center group cursor-pointer"
>
<input
type="checkbox"
checked={router.query.category === cat.id}
readOnly
className="w-5 h-5 rounded-md border-slate-200 text-emerald-500 focus:ring-emerald-500 transition-all cursor-pointer"
/>
<span className="ml-3 text-sm font-medium text-slate-600 group-hover:text-emerald-600 transition-colors">{cat.name}</span>
</Link>
))}
</div>
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-sm">
<div className="flex items-center justify-between mb-6">
<h3 className="font-bold text-lg">Filters</h3>
<BaseIcon path={mdiFilterVariant} size={20} className="text-slate-400" />
</div>
<div className="space-y-4 pt-6 border-t border-slate-100">
<h4 className="text-xs font-bold text-slate-400 uppercase tracking-widest">Trust Score</h4>
<div className="flex items-center justify-between text-xs font-bold text-slate-400">
<span>Any</span>
<span>80+</span>
<span>95+</span>
<div className="space-y-6">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-3">Availability</label>
<div className="space-y-2">
{['Available Today', 'This Week', 'Next Week'].map(label => (
<label key={label} className="flex items-center text-sm text-slate-600 cursor-pointer hover:text-emerald-600">
<input type="checkbox" className="rounded text-emerald-500 mr-3 border-slate-300 focus:ring-emerald-500" />
{label}
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-3">Reliability Score</label>
<input type="range" min="0" max="100" className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
<div className="flex justify-between text-xs text-slate-400 mt-2">
<span>Any</span>
<span>80+</span>
</div>
</div>
<input type="range" min="0" max="100" className="w-full h-2 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
</div>
</div>
</aside>
{/* Results Area */}
<div className="flex-1 space-y-12">
<div className="flex items-center justify-between border-b border-slate-100 pb-6">
<div className="text-slate-500 font-medium">
{loading ? (
<span>Searching professionals...</span>
) : (
<>Found <span className="text-slate-900 font-black">{businesses ? businesses.length : 0} verified</span> professionals</>
)}
</div>
<div className="flex items-center text-sm font-bold text-slate-400">
<BaseIcon path={mdiSortVariant} size={16} className="mr-2" />
Sort by: <span className="text-slate-900 cursor-pointer hover:text-emerald-500 transition-colors ml-1">Reliability Score</span>
<main className="flex-grow">
<div className="flex items-baseline justify-between mb-8">
<h2 className="text-2xl font-bold">
{loading ? 'Searching...' : `${businesses.length} Results for "${searchQueryParam || 'Businesses'}"`}
</h2>
<div className="text-sm text-slate-500 font-medium">
Sort by: <span className="text-slate-900 cursor-pointer hover:text-emerald-500">Reliability Score</span>
</div>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 opacity-50">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-80 bg-slate-100 rounded-[2.5rem] animate-pulse"></div>
))}
<div className="flex justify-center py-20">
<LoadingSpinner />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{businesses && businesses.length > 0 ? (
businesses.map((biz: any) => (
<Link key={biz.id} href={`/public/businesses-details?id=${biz.id}`} className="group bg-white rounded-[2.5rem] border border-slate-100 hover:border-emerald-100 hover:shadow-2xl hover:shadow-emerald-500/10 transition-all overflow-hidden flex flex-col">
<div className="h-48 bg-slate-100 relative overflow-hidden">
{biz.business_photos?.[0]?.photo_url ? (
<img src={biz.business_photos[0].photo_url} alt={biz.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
<div className="grid gap-6">
{businesses.map((biz: any) => (
<Link key={biz.id} href={`/public/businesses-details?id=${biz.id}`}>
<div className="group bg-white rounded-3xl border border-slate-200 hover:border-emerald-500 hover:shadow-xl transition-all overflow-hidden flex flex-col md:flex-row">
{/* Image */}
<div className="md:w-64 h-48 md:h-auto bg-slate-100 relative">
{getBusinessImage(biz) ? (
<img
src={getBusinessImage(biz)}
alt={biz.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-slate-300">
<BaseIcon path={mdiShieldCheck} size={48} />
<div className="absolute inset-0 flex items-center justify-center text-slate-300">
<BaseIcon path={mdiShieldCheck} size={64} />
</div>
)}
{biz.reliability_score >= 80 && (
<div className="absolute top-4 left-4 bg-emerald-500 text-white text-[10px] font-black uppercase tracking-widest px-2 py-1 rounded-md shadow-lg">
Top Rated
</div>
)}
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur px-3 py-1 rounded-full text-xs font-black text-emerald-600 flex items-center shadow-sm">
<BaseIcon path={mdiStar} size={16} className="mr-1" />
{biz.rating || '4.9'}
</div>
</div>
<div className="p-8 flex-1 flex flex-col justify-between">
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-black text-slate-900 group-hover:text-emerald-600 transition-colors line-clamp-1">{biz.name}</h3>
{biz.is_active && (
<BaseIcon path={mdiShieldCheck} size={20} className="text-emerald-500" />
)}
<div className="p-8 flex-grow">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-2xl font-bold group-hover:text-emerald-600 transition-colors mb-1">{biz.name}</h3>
<div className="flex items-center text-slate-500 text-sm">
<BaseIcon path={mdiMapMarker} size={16} className="mr-1" />
{biz.city}, {biz.state} {biz.address}
</div>
</div>
<div className="flex items-center text-slate-400 text-sm font-bold mb-4">
<BaseIcon path={mdiMapMarker} size={16} className="mr-1" />
{biz.city || biz.locations?.[0]?.city || 'Verified Professional'}
<div className="text-right">
<div className="flex items-center justify-end text-emerald-500 font-bold text-xl mb-1">
<BaseIcon path={mdiStar} size={24} className="mr-1 text-amber-400" />
{biz.rating || ((biz.reliability_score || 0) / 20).toFixed(1)}
</div>
<div className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">Reliability Score</div>
</div>
</div>
<div className="pt-6 border-t border-slate-50 flex items-center justify-between">
<span className="text-xs font-black text-emerald-500 uppercase tracking-widest">Top Professional</span>
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-slate-300 group-hover:bg-emerald-500 group-hover:text-white transition-all">
<BaseIcon path={mdiChevronRight} size={20} />
<p className="text-slate-600 line-clamp-2 mb-6 leading-relaxed">
{biz.description || 'Verified service professional providing high-quality solutions for your needs.'}
</p>
<div className="flex flex-wrap gap-4 pt-6 border-t border-slate-100">
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiClockOutline} size={16} className="mr-2 text-emerald-500" />
~{biz.response_time_median_minutes || 30}m Response
</div>
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiCurrencyUsd} size={16} className="mr-2 text-emerald-500" />
Fair Pricing
</div>
<div className="flex items-center text-xs font-bold text-slate-500 uppercase tracking-wider">
<BaseIcon path={mdiShieldCheck} size={16} className="mr-2 text-emerald-500" />
Verified
</div>
</div>
</div>
</Link>
))
) : (
<div className="col-span-full py-24 text-center">
</div>
</Link>
))}
{businesses.length === 0 && !loading && (
<div className="bg-white rounded-3xl p-20 text-center border-2 border-dashed border-slate-200">
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-6 text-slate-300">
<BaseIcon path={mdiMagnify} size={40} />
</div>
<h3 className="text-2xl font-black text-slate-900 mb-2">No results found</h3>
<p className="text-slate-500">Try adjusting your filters or searching for something else.</p>
<h3 className="text-xl font-bold mb-2">No businesses found</h3>
<p className="text-slate-500">Try adjusting your search terms or location.</p>
</div>
)}
</div>
)}
</div>
</main>
</div>
</SectionMain>
</div>
</div>
);
};
SearchPage.getLayout = function getLayout(page: ReactElement) {
SearchView.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default SearchPage;
export default SearchView;

View File

@ -1,73 +1,206 @@
import React from 'react';
import Head from 'next/head';
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import LayoutGuest from '../layouts/Guest';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
const TermsOfUsePage = () => {
export default function PrivacyPolicy() {
const title = 'Crafted Network';
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
setProjectUrl(location.origin);
}, []);
const Information = () => {
return (
<>
<h3>1. Acceptance of Terms</h3>
<div className=''>
<p>
By accessing and using our application, you agree to comply with and
be bound by these Terms of Use. If you do not agree to these terms,
please do not use the application.
</p>
</div>
</>
);
};
const ChangesTerms = () => {
return (
<>
<h3>2. Changes to Terms</h3>
<p>
We reserve the right to modify these Terms of Use at any time. Any
changes will be effective immediately upon posting. Your continued use
of the application after any such changes constitutes your acceptance
of the new terms.
</p>
</>
);
};
const UseApplication = () => {
return (
<>
<h3>3. Use of the Application</h3>
<p>
You agree to use the application only for lawful purposes and in a way
that does not infringe the rights of, restrict, or inhibit anyone
elses use and enjoyment of the application. Prohibited behavior
includes harassing or causing distress or inconvenience to any other
user, transmitting obscene or offensive content, or disrupting the
normal flow of dialogue within the application.
</p>
</>
);
};
const IntellectualProperty = () => {
return (
<>
<h3>4. Intellectual Property</h3>
<p>
All content included on the application, such as text, graphics,
logos, images, and software, is the property of {title} or
its content suppliers and protected by international copyright laws.
Unauthorized use of the content may violate copyright, trademark, and
other laws.
</p>
</>
);
};
const UserContent = () => {
return (
<>
<h3>5. User Content</h3>
<p>
You are responsible for any content you upload, post, or otherwise
make available through the application. You grant {title}
a worldwide, irrevocable, non-exclusive, royalty-free license to use,
reproduce, modify, publish, and distribute such content for any
purpose.
</p>
</>
);
};
const Privacy = () => {
return (
<>
<h3>6. Privacy</h3>
<p>
Your privacy is important to us. Please review our Privacy Policy to
understand our practices regarding the collection, use, and disclosure
of your personal information.
</p>
</>
);
};
const Liability = () => {
return (
<>
<h3>7. Limitation of Liability</h3>
<p>
The application is provided as is and as available without any
warranties of any kind, either express or implied. {title}
does not warrant that the application will be uninterrupted or
error-free. In no event shall {title} be liable for any
damages arising out of your use of the application.
</p>
</>
);
};
const Indemnification = () => {
return (
<>
<h3>8. Indemnification</h3>
<p>
You agree to indemnify, defend, and hold harmless {title},
its officers, directors, employees, and agents from and against any
claims, liabilities, damages, losses, and expenses, including without
limitation reasonable legal and accounting fees, arising out of or in
any way connected with your access to or use of the application or
your violation of these Terms of Use.
</p>
</>
);
};
const Termination = () => {
return (
<>
<h3>9. Termination</h3>
<p>
We reserve the right to terminate or suspend your access to the
application at our sole discretion, without notice and without
liability, for any reason, including if we believe you have violated
these Terms of Use.
</p>
</>
);
};
const GoverningLaw = () => {
return (
<>
<h3>10. Governing Law</h3>
<p>
These Terms of Use are governed by and interpreted in accordance with
applicable laws, without regard to any conflict of law principles. You
agree to submit to the exclusive jurisdiction of the courts that have
authority to resolve any dispute arising from the use of the
application.
</p>
</>
);
};
const ContactUs = () => {
return (
<>
<h3>11. Contact Information</h3>
<p>
If you have any questions about these Terms of Use, please contact us
at: <a href='mailto:support@flatlogic.com'> [support@flatlogic.com]</a>
</p>
</>
);
};
return (
<>
<div className='prose prose-slate mx-auto max-w-none'>
<Head>
<title>{getPageTitle('Terms of Use')} | {title}</title>
<title>{getPageTitle('Terms of Use')}</title>
</Head>
<SectionMain className="py-32">
<div className="max-w-3xl mx-auto space-y-12 animate-fade-in">
<div className="space-y-4">
<h1 className="text-5xl font-black text-slate-900 tracking-tight leading-tight">Terms of Use</h1>
<p className="text-slate-500 font-medium italic">Last updated: February 17, 2026</p>
</div>
<div className='flex justify-center'>
<div className='z-10 md:w-10/12 my-4 bg-white border border-pavitra-400 rounded'>
<div className='p-8 lg:px-12 lg:py-10'>
<h1>Terms of Use</h1>
<div className="prose prose-slate max-w-none text-slate-600 space-y-8 font-medium leading-relaxed">
<p className="text-xl text-slate-900 font-bold">
Welcome to the Crafted Network. By accessing our platform, you agree to the following terms and conditions.
</p>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">1. Acceptance of Terms</h2>
<p>
By using the <span>{title}</span> platform, you signify your agreement to these Terms of Use and our Privacy Policy. If you do not agree to any of these terms, please do not use the platform.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">2. Service Description</h2>
<p>
The <span>{title}</span> provides a directory service connecting service professionals with potential clients. We do not provide the services listed on the platform and are not responsible for the performance or quality of those services.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">3. User Responsibilities</h2>
<p>
Users are responsible for verifying the credentials and reputation of any service professional they choose to engage. Professionals are responsible for providing accurate and honest information about their services and verification status.
</p>
</div>
<div className="space-y-4">
<h2 className="text-2xl font-black text-slate-900">4. Intellectual Property</h2>
<p>
All content on the platform, including logos, designs, and text, is the property of the Crafted Network and is protected by intellectual property laws.
</p>
</div>
<div className="space-y-4 border-t border-slate-100 pt-12">
<p className="text-sm font-bold text-slate-400 uppercase tracking-widest">
© 2026 Crafted Network. Built with Trust & Transparency.
</p>
</div>
<Information />
<ChangesTerms />
<UseApplication />
<IntellectualProperty />
<UserContent />
<Privacy />
<Liability />
<Indemnification />
<Termination />
<GoverningLaw />
<ContactUs />
</div>
</div>
</SectionMain>
</>
</div>
</div>
);
};
}
TermsOfUsePage.getLayout = function getLayout(page: ReactElement) {
PrivacyPolicy.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
export default TermsOfUsePage;

View File

@ -40,21 +40,6 @@ export const loginUser = createAsyncThunk(
}
);
export const registerUser = createAsyncThunk(
'auth/registerUser',
async (creds: Record<string, string>, { rejectWithValue }) => {
try {
const response = await axios.post('auth/signup', creds);
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const passwordReset = createAsyncThunk(
'auth/passwordReset',
async (value: Record<string, string>, { rejectWithValue }) => {
@ -112,19 +97,6 @@ export const authSlice = createSlice({
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false;
});
builder.addCase(registerUser.pending, (state) => {
state.isFetching = true;
});
builder.addCase(registerUser.fulfilled, (state) => {
state.isFetching = false;
state.errorMessage = '';
});
builder.addCase(registerUser.rejected, (state, action) => {
state.errorMessage = String(action.payload) || 'Something went wrong. Try again';
state.isFetching = false;
});
builder.addCase(findMe.pending, () => {
console.log('Pending findMe');
});
@ -149,4 +121,4 @@ export const authSlice = createSlice({
// Action creators are generated for each case reducer function
export const { logoutUser } = authSlice.actions;
export default authSlice.reducer;
export default authSlice.reducer;