ooo
This commit is contained in:
@@ -18,7 +18,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
const [customDomain, setCustomDomain] = useState('');
|
||||
const [customSubdomain, setCustomSubdomain] = useState('');
|
||||
const [domainType, setDomainType] = useState('domain'); // 'domain' or 'subdomain'
|
||||
|
||||
|
||||
// Domain validation states
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
||||
@@ -30,7 +30,8 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
const [dnsVerified, setDnsVerified] = useState({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false
|
||||
a: false,
|
||||
ip: false
|
||||
});
|
||||
|
||||
// Form validation
|
||||
@@ -42,15 +43,6 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
// File upload reference
|
||||
const fileInputRef = React.useRef(null);
|
||||
|
||||
// Function to clean domain input
|
||||
const cleanDomainInput = (input) => {
|
||||
// Remove http://, https://, www., and trailing slashes
|
||||
return input
|
||||
.replace(/^(https?:\/\/)?(www\.)?/i, '')
|
||||
.replace(/\/+$/, '')
|
||||
.trim();
|
||||
};
|
||||
|
||||
// Effect for handling domain type changes
|
||||
useEffect(() => {
|
||||
if (!useCustomDomain) {
|
||||
@@ -61,7 +53,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
}
|
||||
|
||||
validateForm();
|
||||
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, domainType, dnsMethod]);
|
||||
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, dnsVerified.ns, domainType, dnsMethod]);
|
||||
|
||||
// Show toast notification
|
||||
const showToast = (message) => {
|
||||
@@ -110,7 +102,8 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
setDnsVerified({
|
||||
cname: false,
|
||||
ns: false,
|
||||
a: false
|
||||
a: false,
|
||||
ip: false
|
||||
});
|
||||
} else {
|
||||
// Force SiliconPin subdomain to be checked if custom domain is checked
|
||||
@@ -138,12 +131,12 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
|
||||
// Handle domain and subdomain input changes
|
||||
const handleDomainChange = (e) => {
|
||||
const cleanedValue = cleanDomainInput(e.target.value);
|
||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?(www\.)?/i, '').replace(/\/+$/, '').trim();
|
||||
setCustomDomain(cleanedValue);
|
||||
};
|
||||
|
||||
const handleSubdomainChange = (e) => {
|
||||
const cleanedValue = cleanDomainInput(e.target.value);
|
||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?/i, '').replace(/\/+$/, '').trim();
|
||||
setCustomSubdomain(cleanedValue);
|
||||
};
|
||||
|
||||
@@ -187,6 +180,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
setValidationMessage('');
|
||||
setShowDnsConfig(false);
|
||||
|
||||
<<<<<<< HEAD
|
||||
fetch('/validate-domain', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -206,6 +200,13 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
.then(data => {
|
||||
const checkResult = data.status === "success";
|
||||
console.log("finding:: checkResult:: ", checkResult, data);
|
||||
=======
|
||||
// Simulate an API call to validate the domain
|
||||
setTimeout(() => {
|
||||
// Simulate a real domain check - in a real app this would be an API call
|
||||
// call /host-api/v1/domains/validate/?domain=domain.com
|
||||
const checkResult = true; // Assume domain is valid for demo
|
||||
>>>>>>> origin/staging
|
||||
|
||||
setIsValidating(false);
|
||||
setIsValidDomain(checkResult);
|
||||
@@ -217,6 +218,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
setValidationMessage('Domain appears to be unregistered or unavailable.');
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
validateForm();
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -226,9 +228,45 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
setValidationMessage('Error checking domain. Please try again.');
|
||||
validateForm();
|
||||
});
|
||||
=======
|
||||
validateForm();
|
||||
}, 500);
|
||||
>>>>>>> origin/staging
|
||||
};
|
||||
|
||||
// Check DNS configuration
|
||||
const checkSubDomainCname = () => {
|
||||
const domainToCheck = customDomain || customSubdomain;
|
||||
|
||||
fetch('http://localhost:2058/host-api/v1/check-c-name/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domainToCheck)}`
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
checkDnsConfig('cname');
|
||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||
console.log('CNAME record:', data.cname);
|
||||
// Handle success - update UI
|
||||
} else {
|
||||
console.error('Error:', data.message);
|
||||
// Handle error
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch error:', error);
|
||||
// Show error to user
|
||||
});
|
||||
};
|
||||
const checkDnsConfig = (type) => {
|
||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||
|
||||
@@ -268,6 +306,10 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
setFormValid(false);
|
||||
return;
|
||||
}
|
||||
if (dnsMethod === 'ip' && !dnsVerified.ip) {
|
||||
setFormValid(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setFormValid(true);
|
||||
@@ -283,20 +325,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
}
|
||||
|
||||
// In a real app, this would submit the form data to the server
|
||||
console.log({
|
||||
deploymentType,
|
||||
appType,
|
||||
sampleWebAppType,
|
||||
sourceType,
|
||||
repoUrl,
|
||||
deploymentKey,
|
||||
useSubdomain,
|
||||
useCustomDomain,
|
||||
customDomain,
|
||||
customSubdomain,
|
||||
domainType,
|
||||
dnsMethod
|
||||
});
|
||||
console.log([{ deploymentType, appType, sampleWebAppType, sourceType, repoUrl, deploymentKey, useSubdomain, useCustomDomain, customDomain, customSubdomain, domainType, dnsMethod}]);
|
||||
|
||||
showToast('Form submitted successfully!');
|
||||
};
|
||||
@@ -572,9 +601,10 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
|
||||
{/* Domain Validation */}
|
||||
<button
|
||||
disabled={!customDomain && !customSubdomain}
|
||||
type="button"
|
||||
onClick={validateDomain}
|
||||
className="px-4 py-2 bg-neutral-600 text-white font-medium rounded-md hover:bg-neutral-500 transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500"
|
||||
className={`px-4 py-2 ${ !customDomain && !customSubdomain ? 'bg-neutral-600 cursor-not-allowed' : 'bg-[#6d9e37] focus:ring-[#6d9e37] transition-colors'} text-white font-medium rounded-md transition-colors focus:outline-none`}
|
||||
>
|
||||
Validate Domain
|
||||
</button>
|
||||
@@ -604,7 +634,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
|
||||
{/* CNAME Record Option */}
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<label for="dns-cname" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-cname"
|
||||
@@ -634,7 +664,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkDnsConfig('cname')}
|
||||
onClick={() => (checkSubDomainCname())}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.cname
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
@@ -644,67 +674,114 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Nameserver Option (only for full domains, not subdomains) */}
|
||||
{domainType === 'domain' && (
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ns"
|
||||
name="dns-method"
|
||||
value="ns"
|
||||
checked={dnsMethod === 'ns'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
|
||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||
<>
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<label for="dns-ns" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ns"
|
||||
name="dns-method"
|
||||
value="ns"
|
||||
checked={dnsMethod === 'ns'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
|
||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns1.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns1.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns2.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('ns2.siliconpin.com')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
onClick={() => checkDnsConfig('ns')}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.ns
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkDnsConfig('ns')}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsVerified.ns
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||
<label for="dns-ip" className="flex items-start cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="dns-ip"
|
||||
name="dns-method"
|
||||
value="ip"
|
||||
checked={dnsMethod === 'ip'}
|
||||
onChange={handleDnsMethodChange}
|
||||
className="mt-1 mr-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="dns-ip" className="block text-white font-medium">Use Our IP Address</label>
|
||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">xxx.xxx.x.xx</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard('xxx.xxx.x.xx')}
|
||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||
aria-label="Copy nameserver value"
|
||||
>
|
||||
<span className="text-lg">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkDnsConfig('ip')}
|
||||
className={`px-3 py-1 text-white text-sm rounded
|
||||
${dnsMethod === 'ip'
|
||||
? 'bg-green-700 hover:bg-green-600'
|
||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||
>
|
||||
{/* {dnsVerified.ip ? '✓ IP Address Verified' : 'Check IP Address'} */}
|
||||
Proceed to Pay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -714,7 +791,6 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
@@ -728,7 +804,6 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
Start the Deployment
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Toast visible={toast.visible} message={toast.message} />
|
||||
</div>
|
||||
);
|
||||
|
||||
333
src/components/HireAIAgent.tsx
Normal file
333
src/components/HireAIAgent.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
// Define the AI Agent type
|
||||
interface AIAgent {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
capabilities: string[];
|
||||
responseTime: string;
|
||||
pricing: string;
|
||||
useCases: string[];
|
||||
}
|
||||
|
||||
// AI Agent types data
|
||||
const aiAgentTypes: AIAgent[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Code Assistant",
|
||||
description: "AI-powered coding assistant that helps write, debug, and optimize code across multiple languages",
|
||||
capabilities: ["Code Generation", "Code Review", "Bug Detection", "Code Explanation", "Documentation"],
|
||||
responseTime: "Instant",
|
||||
pricing: "$29/month",
|
||||
useCases: ["Software Development", "Web Development", "Mobile Apps", "Enterprise Applications"]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Data Analyst",
|
||||
description: "Specialized AI for data analytics, visualization, and insights extraction",
|
||||
capabilities: ["Data Cleaning", "Statistical Analysis", "Data Visualization", "Predictive Modeling"],
|
||||
responseTime: "Within minutes",
|
||||
pricing: "$49/month",
|
||||
useCases: ["Business Intelligence", "Market Research", "Financial Analysis", "Customer Insights"]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Content Creator",
|
||||
description: "AI agent for generating and optimizing various types of content",
|
||||
capabilities: ["Blog Writing", "Social Media Content", "Email Marketing", "SEO Optimization"],
|
||||
responseTime: "Instant to minutes",
|
||||
pricing: "$39/month",
|
||||
useCases: ["Marketing Teams", "Content Publishers", "Social Media Managers", "Small Businesses"]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "UI/UX Design Assistant",
|
||||
description: "AI that helps create design mockups, wireframes, and UI elements",
|
||||
capabilities: ["Wireframing", "UI Design", "Visual Elements", "Design Feedback"],
|
||||
responseTime: "Within minutes",
|
||||
pricing: "$59/month",
|
||||
useCases: ["Product Teams", "Web Designers", "App Developers", "Creative Agencies"]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Research Assistant",
|
||||
description: "AI agent that helps with research, information gathering, and summarization",
|
||||
capabilities: ["Literature Review", "Data Collection", "Summarization", "Citation Management"],
|
||||
responseTime: "Within minutes",
|
||||
pricing: "$45/month",
|
||||
useCases: ["Academic Research", "Market Research", "Competitive Analysis", "Legal Research"]
|
||||
}
|
||||
];
|
||||
|
||||
export default function HireAIAgent() {
|
||||
const [selectedAgent, setSelectedAgent] = useState<AIAgent | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// Filter AI agents based on search term
|
||||
const filteredAgents = aiAgentTypes.filter(agent =>
|
||||
agent.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
agent.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
agent.capabilities.some(cap => cap.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||
agent.useCases.some(useCase => useCase.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<h1 className="text-center text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Hire an AI Agent</h1>
|
||||
<p className="text-center text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300 mb-10">
|
||||
Leverage our AI agents to automate tasks, generate content, analyze data, and more
|
||||
</p>
|
||||
|
||||
{/* Hero Banner */}
|
||||
<div className="bg-neutral-800 text-white rounded-lg p-8 mb-12">
|
||||
<div className="flex flex-col md:flex-row md:items-center">
|
||||
<div className="md:w-2/3 mb-6 md:mb-0 md:pr-6">
|
||||
<h2 className="text-2xl md:text-3xl font-bold mb-4 text-[#6d9e37]">Why Choose AI Agents?</h2>
|
||||
<p className="mb-4">
|
||||
Our AI agents work 24/7, provide instant results, and cost a fraction of human labor. They excel at repetitive tasks, data processing, and creative content generation.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 mt-6">
|
||||
<div className="flex items-start">
|
||||
<div className="bg-white bg-opacity-20 p-2 rounded mr-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">Instant Results</h3>
|
||||
<p className="text-xs text-zinc-300">No waiting for human availability</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="bg-white bg-opacity-20 p-2 rounded mr-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">Cost Effective</h3>
|
||||
<p className="text-xs text-zinc-300">Fraction of human labor costs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:w-1/3">
|
||||
<div className="bg-white bg-opacity-10 p-6 rounded-lg text-center">
|
||||
<h3 className="font-bold mb-2">Start with an AI Agent Today</h3>
|
||||
<p className="text-sm mb-4 text-zinc-300">
|
||||
All plans come with a 7-day free trial
|
||||
</p>
|
||||
<Button className="w-full">View All Agents</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-8">
|
||||
<div className="max-w-md mx-auto">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search AI agents by capability or use case..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Agent Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredAgents.map((agent) => (
|
||||
<Card key={agent.id} className="hover:shadow-md transition">
|
||||
<CardHeader>
|
||||
<CardTitle>{agent.title}</CardTitle>
|
||||
<CardDescription>{agent.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold mb-2">Key Capabilities:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{agent.capabilities.map((capability, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
|
||||
>
|
||||
{capability}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Response Time:</span>
|
||||
<span className="font-medium text-green-600">{agent.responseTime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Pricing:</span>
|
||||
<span className="font-medium">{agent.pricing}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full" onClick={() => {setSelectedAgent(agent); setDialogOpen(true);}}>View Details</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI Agent Details Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
{selectedAgent && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedAgent.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedAgent.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">Capabilities:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAgent.capabilities.map((capability, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
|
||||
>
|
||||
{capability}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">Common Use Cases:</h4>
|
||||
<ul className="list-disc pl-5 text-zinc-600 text-sm">
|
||||
{selectedAgent.useCases.map((useCase, index) => (
|
||||
<li key={index}>{useCase}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">Response Time:</h4>
|
||||
<p className="font-medium text-green-600">{selectedAgent.responseTime}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">Pricing:</h4>
|
||||
<p className="font-medium">{selectedAgent.pricing}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<h4 className="text-sm font-semibold mb-1">Subscription Includes:</h4>
|
||||
<ul className="text-sm text-zinc-600 space-y-1">
|
||||
<li className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Unlimited requests
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
24/7 availability
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
API access
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-green-500 mr-2" viewBox="0 0 20 20" fill="#6d9e37">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Export capabilities
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button">
|
||||
Subscribe Now
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Comparison Section */}
|
||||
<div className="mt-16">
|
||||
<h2 className="text-2xl font-bold mb-8 text-center">AI Agents vs. Human Developers</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Feature</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">AI Agents</th>
|
||||
<th className="p-4 text-center border-[1px] border-[#6d9e37] w-1/3">Human Developers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Availability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">24/7, instant access</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited by working hours and availability</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Low monthly subscription</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher hourly or project-based rates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Task Complexity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Best for repetitive, well-defined tasks</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Better for complex, novel problems</td>
|
||||
</tr>
|
||||
<tr className="bg-neutral-800 text-white">
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Creativity</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Limited to patterns in training data</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Higher level of creativity and innovation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37] font-medium">Scalability</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Highly scalable at no extra cost</td>
|
||||
<td className="p-4 border-[1px] border-[#6d9e37]">Requires additional hiring and onboarding</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Section */}
|
||||
<div className="mt-16 bg-neutral-800 p-8 rounded-lg text-center">
|
||||
<h2 className="text-2xl font-bold mb-4 text-[#6d9e37]">Not sure which AI agent is right for you?</h2>
|
||||
<p className="text-neutral-300 max-w-2xl mx-auto mb-8">
|
||||
Our AI solution experts can help you determine which AI agent best fits your specific needs and use cases.
|
||||
</p>
|
||||
<Button size="lg">
|
||||
Schedule a Consultation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
263
src/components/HireDeveloper.tsx
Normal file
263
src/components/HireDeveloper.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||
|
||||
// Define the Developer type
|
||||
interface Developer {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
skills: string[];
|
||||
availability: "High" | "Medium" | "Low";
|
||||
minBudget: string;
|
||||
contractFee: string;
|
||||
}
|
||||
|
||||
// Developer types data
|
||||
const developerTypes: Developer[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Web Developer",
|
||||
description: "Frontend, backend or full-stack developers specialized in web technologies",
|
||||
skills: ["React", "Angular", "Vue", "Node.js", "Django", "Ruby on Rails", "Laravel", "Express", "Flask", "PHP", "JavaScript", "TypeScript", "HTML", "CSS", "SASS", "LESS", "Bootstrap", "Tailwind CSS", "Material-UI", "Ant Design", "REST APIs", "GraphQL", "WebSockets", "OAuth", "JWT", "WebRTC", "PWA", "SEO", "Accessibility", "Performance", "Security"],
|
||||
availability: "High",
|
||||
minBudget: "Budget-friendly",
|
||||
contractFee: "Nominal"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "App Developer",
|
||||
description: "iOS and Android native or cross-platform mobile app developers",
|
||||
skills: ["Swift", "Kotlin", "React Native", "Flutter", "Xamarin", "Ionic", "PhoneGap", "Cordova", "NativeScript", "Appcelerator", "Java", "Objective-C", "Firebase", "Realm", "Core Data", "SQLite", "Push Notifications", "In-App Purchases", "ARKit", "Core ML", "Android Jetpack", "Material Design", "Google Play Services", "App Store Connect", "Google Play Console"],
|
||||
availability: "Medium",
|
||||
minBudget: "Limited",
|
||||
contractFee: "Modest"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "C/C++ Developer",
|
||||
description: "Systems programmers and embedded systems specialists",
|
||||
skills: ["C", "C++", "Embedded Systems", "Linux Kernel", "Real-time Systems", "RTOS", "Device Drivers", "Firmware", "Microcontrollers", "ARM", "AVR", "PIC", "Raspberry Pi", "Arduino", "BeagleBone", "Zephyr", "FreeRTOS", "VxWorks", "QNX", "Bare Metal"],
|
||||
availability: "Low",
|
||||
minBudget: "Increased",
|
||||
contractFee: "Considerable"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Go Developer",
|
||||
description: "Backend and systems engineers specializing in Go language",
|
||||
skills: ["Go", "Microservices", "Docker", "Kubernetes", "API Development", "gRPC", "Protobuf", "WebSockets", "WebAssembly", "Concurrency", "Performance Tuning", "Module Building", "Error Handling", "Testing", "Security"],
|
||||
availability: "Medium",
|
||||
minBudget: "Boosted",
|
||||
contractFee: "Elevated"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Machine Learning Engineer",
|
||||
description: "AI and ML specialists for implementing intelligent solutions",
|
||||
skills: ["Python", "TensorFlow", "PyTorch", "Data Science", "MLOps", "NLP", "Computer Vision", "Reinforcement Learning", "Time Series Analysis", "Anomaly Detection", "Recommendation Systems", "Predictive Modeling", "Deep Learning", "Fraud Detection", "Sentiment Analysis", "Speech Recognition", "Image Processing", "Object Detection", "Generative Models", "AutoML", "Image Recognition", "Text Classification", "Chatbots", "AI Ethics"],
|
||||
availability: "Low",
|
||||
minBudget: "Premium",
|
||||
contractFee: "Lavish"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "DevOps Engineer",
|
||||
description: "Specialists in CI/CD, cloud infrastructure, and deployment automation",
|
||||
skills: ["AWS", "Azure", "GCP", "Terraform", "Ansible", "Jenkins", "Docker", "Kubernetes", "GitOps", "Monitoring", "Logging", "Security", "Compliance", "Scalability", "Reliability", "Resilience", "Performance", "Cost Optimization", "Disaster Recovery", "Incident Response", "SRE"],
|
||||
availability: "Medium",
|
||||
minBudget: "Pricier",
|
||||
contractFee: "Substantial"
|
||||
}
|
||||
];
|
||||
|
||||
export default function HireHumanDeveloper() {
|
||||
const [selectedType, setSelectedType] = useState<Developer | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
// Filter developers based on search term
|
||||
const filteredDevelopers = developerTypes.filter(dev =>
|
||||
dev.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
dev.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
dev.skills.some(skill => skill.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-12">
|
||||
<h1 className="text-center text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Hire a Human Developer</h1>
|
||||
<p className="text-center text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300 mb-10">
|
||||
Connect with skilled developers across various technologies and specialties
|
||||
</p>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className="mb-8">
|
||||
<div className="max-w-md mx-auto">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by technology, skill, or type..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Developer Types Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredDevelopers.map((developer) => (
|
||||
<Card key={developer.id} className="hover:shadow-md transition">
|
||||
<CardHeader>
|
||||
<CardTitle>{developer.title}</CardTitle>
|
||||
<CardDescription>{developer.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold mb-2 text-neutral-300">Key Skills:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{developer.skills.map((skill, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-neutral-300 text-zinc-800 text-xs px-2 py-1 rounded"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Availability:</span>
|
||||
<span className={`font-medium ${
|
||||
developer.availability === "High" ? "text-green-600" :
|
||||
developer.availability === "Medium" ? "text-amber-600" : "text-red-600"
|
||||
}`}>
|
||||
{developer.availability}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Starting at:</span>
|
||||
<span className="font-medium">{developer.minBudget}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-zinc-500">Contract Fee:</span>
|
||||
<span className="font-medium">{developer.contractFee}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setSelectedType(developer);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Hire Developer
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Developer Details Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
{selectedType && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedType.title} Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review the details and proceed to hire
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Overview:</h4>
|
||||
<p className="text-neutral-400">{selectedType.description}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Specialized Skills:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedType.skills.map((skill, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-block bg-zinc-100 text-zinc-800 text-xs px-2 py-1 rounded"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Availability:</h4>
|
||||
<p className={`font-medium ${
|
||||
selectedType.availability === "High" ? "text-green-600" :
|
||||
selectedType.availability === "Medium" ? "text-amber-600" : "text-red-600"
|
||||
}`}>
|
||||
{selectedType.availability}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Minimum Budget:</h4>
|
||||
<p className="font-medium text-neutral-950">{selectedType.minBudget}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Contract Fee:</h4>
|
||||
<p className="font-medium text-neutral-950">{selectedType.contractFee}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1 text-neutral-950">Usual Turnaround:</h4>
|
||||
<p className="font-medium text-neutral-950">1-3 weeks</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button">
|
||||
Proceed to Hire
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Additional Information */}
|
||||
<div className="mt-16 bg-neutral-800 p-8 rounded-lg">
|
||||
<h2 className="text-2xl font-bold mb-4 text-[#6d9e37]">Why Hire Our Human Developers?</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Vetted Experts</h3>
|
||||
<p className="text-[#fff]">
|
||||
All our developers go through a rigorous vetting process to ensure top-quality talent.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Flexible Engagement</h3>
|
||||
<p className="text-[#fff]">
|
||||
Hire on hourly, project-based, or long-term contracts based on your needs.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 text-[#6d9e37]">Satisfaction Guarantee</h3>
|
||||
<p className="text-[#fff]">
|
||||
Not satisfied with the talent? We'll match you with another developer at no extra cost.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
280
src/components/Login.tsx
Normal file
280
src/components/Login.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import PocketBase from 'pocketbase';
|
||||
import { Input } from "./ui/input";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
||||
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
interface AuthStatus {
|
||||
message: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
interface UserRecord {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
record: UserRecord;
|
||||
}
|
||||
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('suvodip@siliconpin.com');
|
||||
const [password, setPassword] = useState('Simple2pass');
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
record: {
|
||||
query: string;
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setStatus({ message: '', isError: false });
|
||||
|
||||
try {
|
||||
const authData = await pb.collection("users").authWithPassword(email, password);
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
setStatus({
|
||||
message: "Login failed. Please check your credentials.",
|
||||
isError: true
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loginWithOAuth2 = async (provider: 'google' | 'facebook' | 'github') => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setStatus({ message: '', isError: false });
|
||||
|
||||
const authData = await pb.collection('users').authWithOAuth2({ provider });
|
||||
|
||||
if (!authData?.record) {
|
||||
throw new Error("No user record found");
|
||||
}
|
||||
|
||||
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
|
||||
const authResponse: AuthResponse = {
|
||||
token: authData.token,
|
||||
record: {
|
||||
query: 'new',
|
||||
id: authData.record.id,
|
||||
email: authData.record.email || '',
|
||||
name: authData.record.name || '',
|
||||
avatar: authData.record.avatar || ''
|
||||
}
|
||||
};
|
||||
|
||||
await syncSessionWithBackend(authResponse, avatarUrl);
|
||||
window.location.href = '/profile';
|
||||
} catch (error) {
|
||||
console.error(`${provider} Login failed:`, error);
|
||||
setStatus({
|
||||
message: `${provider} login failed. Please try again.`,
|
||||
isError: true
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/users/session/', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Important for cookies
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: 'new',
|
||||
accessToken: authData.token,
|
||||
email: authData.record.email,
|
||||
name: authData.record.name,
|
||||
avatar: avatarUrl,
|
||||
isAuthenticated: true,
|
||||
id: authData.record.id
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to sync session');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Session synced with backend:', data);
|
||||
} catch (error) {
|
||||
console.error('Error syncing session:', error);
|
||||
throw error; // Re-throw the error if you want calling functions to handle it
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
|
||||
<CardHeader className="text-center space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Welcome Back</CardTitle>
|
||||
<CardDescription className="">
|
||||
Sign in to access your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{status.message && (
|
||||
<div className={`p-3 rounded-md text-sm ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="">
|
||||
Email Address
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
className="focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label htmlFor="password" className="">
|
||||
Password
|
||||
</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
className="text-sm text-[#6d9e37]"
|
||||
>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={passwordVisible ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="focus:ring-2 focus:ring-blue-500 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{passwordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
'Sign In'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<Separator className="w-full" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => loginWithOAuth2('google')}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<img src="/assets/google.svg" alt="Google" className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => loginWithOAuth2('facebook')}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<img src="/assets/facebook.svg" alt="Facebook" className="h-6 w-6" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => loginWithOAuth2('github')}
|
||||
disabled={isLoading}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<div className="px-6 pb-6 text-center text-sm text-gray-500">
|
||||
Don't have an account?{' '}
|
||||
<a href="/sign-up" className="font-medium text-[#6d9e37] hover:underline">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -36,7 +36,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>{feature}</span>
|
||||
<span className='text-zinc-600'>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
52
src/components/UpdateAvatar.tsx
Normal file
52
src/components/UpdateAvatar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { X } from "lucide-react"; // Import an icon for the remove button
|
||||
|
||||
export function AvatarUpload() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setSelectedFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
const handleRemoveFile = () => {
|
||||
setSelectedFile(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''; // Reset file input
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label
|
||||
htmlFor="avatar"
|
||||
className="bg-primary hover:bg-primary/90 text-primary-foreground py-2 px-4 text-sm rounded-md cursor-pointer transition-colorsfocus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
Change Avatar
|
||||
</Label>
|
||||
<Input type="file" id="avatar" ref={fileInputRef} accept="image/jpeg,image/png,image/gif" className="hidden" onChange={handleFileChange}/>
|
||||
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>Browse</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
JPG, GIF or PNG. 1MB max.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 space-x-2">
|
||||
<div className="truncate max-w-[200px]">
|
||||
<p className="text-sm font-medium truncate">{selectedFile.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{(selectedFile.size / 1024).toFixed(2)} KB</p>
|
||||
</div>
|
||||
<Button size="sm" className="text-xs p-1 h-fit">Update</Button>
|
||||
<Button size="sm" onClick={handleRemoveFile} className="bg-red-500 hover:bg-red-600 text-xs p-1 h-fit">Remove</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default AvatarUpload;
|
||||
273
src/components/UserProfile.tsx
Normal file
273
src/components/UserProfile.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import UpdateAvatar from './UpdateAvatar';
|
||||
|
||||
interface SessionData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
success: boolean;
|
||||
session_data: SessionData;
|
||||
user_avatar: string;
|
||||
}
|
||||
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [userData, setUserData] = useState<UserData | null>(null);
|
||||
const [invoiceList, setInvoiceList] = useState<any[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSessionData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'http://localhost:2058/host-api/v1/users/get-profile-data/',
|
||||
{
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Session fetch failed');
|
||||
}
|
||||
setUserData(data);
|
||||
return data.session_data;
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const getInvoiceListData = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
||||
method: 'GET',
|
||||
credentials: 'include', // Crucial for cookies
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Session fetch failed');
|
||||
}
|
||||
|
||||
setInvoiceList(data.data); // Fix: Use `data.data` instead of `data`
|
||||
return data.data; // Fix: `session_data` does not exist in response
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
fetchSessionData();
|
||||
getInvoiceListData();
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
return <div>Loading profile data...</div>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6 container mx-auto">
|
||||
{/* <div>
|
||||
<h3 className="text-lg font-medium">Profile</h3>
|
||||
<p className="text-sm text-muted-foreground">Update your profile settings.</p>
|
||||
</div> */}
|
||||
<Separator />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<div className="flex-1 lg:max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Personal Information</CardTitle>
|
||||
<CardDescription>
|
||||
Update your personal information and avatar.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={userData.session_data?.user_avatar} />
|
||||
<AvatarFallback>JP</AvatarFallback>
|
||||
</Avatar>
|
||||
<UpdateAvatar />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">Full Name</Label>
|
||||
<Input id="firstName" defaultValue={userData.session_data?.user_name || 'Jhon'} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Phone</Label>
|
||||
<Input id="phone" defaultValue={userData.session_data?.user_vatar || ''} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" defaultValue={userData.session_data?.user_email || ''} />
|
||||
</div>
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
defaultValue="I'm a software developer based in New York."
|
||||
className="resize-none"
|
||||
rows={5}
|
||||
/>
|
||||
</div> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Billing Information</CardTitle>
|
||||
<CardDescription>
|
||||
View your billing history.
|
||||
</CardDescription>
|
||||
{/* <CardContent> */}
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Invoice ID</th>
|
||||
<th className="text-left">Date</th>
|
||||
<th className="text-left">Description</th>
|
||||
<th className="text-right">Amount</th>
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* {
|
||||
invoiceList.map((invoice) => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.invoice_id}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.description}</td>
|
||||
<td className="text-right">{invoice.amount}</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
))
|
||||
} */}
|
||||
<tr>
|
||||
<td>dcdicdicib</td>
|
||||
<td>2023-10-01</td>
|
||||
<td>Subscription Fee</td>
|
||||
<td className="text-right">$10.00</td>
|
||||
<td className="text-center"><a href="">Print</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>dcibcdici</td>
|
||||
<td>2023-09-01</td>
|
||||
<td>Subscription Fee</td>
|
||||
<td className="text-right">$10.00</td>
|
||||
<td className="text-center"><a href="" >Print</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* </CardContent> */}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{/* <Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your application preferences.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">Language</Label>
|
||||
<Select defaultValue="english">
|
||||
<SelectTrigger id="language">
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
<SelectItem value="french">French</SelectItem>
|
||||
<SelectItem value="german">German</SelectItem>
|
||||
<SelectItem value="spanish">Spanish</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Select defaultValue="est">
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue placeholder="Select timezone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
|
||||
<SelectItem value="cst">Central Standard Time (CST)</SelectItem>
|
||||
<SelectItem value="mst">Mountain Standard Time (MST)</SelectItem>
|
||||
<SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card> */}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-6 lg:max-w-md">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security</CardTitle>
|
||||
<CardDescription>
|
||||
Update your password and security settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">Current password</Label>
|
||||
<Input id="currentPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">New password</Label>
|
||||
<Input id="newPassword" type="password" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm password</Label>
|
||||
<Input id="confirmPassword" type="password" />
|
||||
</div>
|
||||
<Button className="mt-4">Update password</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="mt-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
These actions are irreversible. Proceed with caution.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
|
||||
<Button variant="outline">Export data</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
src/components/ui/avatar.tsx
Normal file
57
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.HTMLAttributes<HTMLSpanElement> & {
|
||||
size?: "sm" | "md" | "lg"
|
||||
}
|
||||
>(({ className, size = "md", ...props }, ref) => {
|
||||
const sizeClasses = {
|
||||
sm: "h-8 w-8",
|
||||
md: "h-10 w-10",
|
||||
lg: "h-12 w-12",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex shrink-0 overflow-hidden rounded-full",
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Avatar.displayName = "Avatar"
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
HTMLImageElement,
|
||||
React.ImgHTMLAttributes<HTMLImageElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<img
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = "AvatarImage"
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.HTMLAttributes<HTMLSpanElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = "AvatarFallback"
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -8,7 +8,7 @@ export interface ButtonProps
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
({ className, variant = "default", type = '', size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
|
||||
@@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border border-neutral-200 bg-white text-neutral-950 shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
"rounded-lg border border-[#6d9e37] bg-neutral-800 text-[#6d9e37] shadow-sm dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-[#fff] p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-[#fff] transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4 text-black" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight text-[#6d9e37]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
@@ -1,25 +1,130 @@
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface SelectProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<select
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
);
|
||||
Select.displayName = "Select";
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
export { Select };
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
|
||||
"focus:bg-neutral-700 focus:text-white",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
// ChevronDownIcon component
|
||||
const ChevronDownIcon = React.forwardRef<
|
||||
SVGSVGElement,
|
||||
React.SVGProps<SVGSVGElement>
|
||||
>((props, ref) => (
|
||||
<svg
|
||||
ref={ref}
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
));
|
||||
ChevronDownIcon.displayName = "ChevronDownIcon";
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
};
|
||||
23
src/components/ui/separator.tsx
Normal file
23
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "../../lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
orientation?: "horizontal" | "vertical"
|
||||
}
|
||||
>(({ className, orientation = "horizontal", ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
role="separator"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Separator.displayName = "Separator"
|
||||
|
||||
export { Separator }
|
||||
Reference in New Issue
Block a user