first commit
This commit is contained in:
20
.eslintrc.json
Normal file
20
.eslintrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": { "browser": true, "es2020": true },
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"ignorePatterns": ["dist", ".eslintrc.cjs"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
92
.gitignore
vendored
Normal file
92
.gitignore
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Build outputs
|
||||
build/
|
||||
out/
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
|
||||
# Nuxt.js
|
||||
.nuxt/
|
||||
|
||||
# Gatsby
|
||||
.cache/
|
||||
public/
|
||||
|
||||
# Storybook
|
||||
storybook-static
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
590
App.tsx
Normal file
590
App.tsx
Normal file
@@ -0,0 +1,590 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Navigation } from "./components/Navigation";
|
||||
import { HeroSection } from "./components/HeroSection";
|
||||
import { ClientLogos } from "./components/ClientLogos";
|
||||
import { ServicesSection } from "./components/ServicesSection";
|
||||
import { WhyChooseWDI } from "./components/WhyChooseWDI";
|
||||
import { HorizontalTagScroller } from "./components/HorizontalTagScroller";
|
||||
import { CaseStudyHighlight } from "./components/CaseStudyHighlight";
|
||||
import { InlineCTA } from "./components/InlineCTA";
|
||||
import { ProcessSection } from "./components/ProcessSection";
|
||||
import { CarouselTestimonials } from "./components/CarouselTestimonials";
|
||||
import { ResourceCards } from "./components/ResourceCards";
|
||||
import { SplitCallToAction } from "./components/SplitCallToAction";
|
||||
import { Footer } from "./components/Footer";
|
||||
import { Button } from "./components/ui/button";
|
||||
|
||||
// Main Category Pages
|
||||
import { Services } from "./pages/Services";
|
||||
import { Solutions } from "./pages/Solutions";
|
||||
import { Industries } from "./pages/Industries";
|
||||
import { Resources } from "./pages/Resources";
|
||||
import { Company } from "./pages/Company";
|
||||
import { ContactMain } from "./pages/ContactMain";
|
||||
import { WebCloudServices } from "./pages/WebCloudServices";
|
||||
import { SoftwareEngineering } from "./pages/SoftwareEngineering";
|
||||
import { DesignExperience } from "./pages/DesignExperience";
|
||||
import { ArtificialIntelligenceServices } from "./pages/ArtificialIntelligenceServices";
|
||||
import { MachineLearning } from "./pages/MachineLearning";
|
||||
|
||||
// Service Pages
|
||||
import { MobileAppDevelopment } from "./pages/MobileAppDevelopment";
|
||||
import { IOSAppDevelopment } from "./pages/iOSAppDevelopment";
|
||||
import { AndroidAppDevelopment } from "./pages/AndroidAppDevelopment";
|
||||
import { CrossPlatformAppDevelopment } from "./pages/CrossPlatformAppDevelopment";
|
||||
import { NativeAppDevelopment } from "./pages/NativeAppDevelopment";
|
||||
import { PWADevelopment } from "./pages/PWADevelopment";
|
||||
import { WearableDeviceDevelopment } from "./pages/WearableDeviceDevelopment";
|
||||
import { CustomWebAppDevelopment } from "./pages/CustomWebAppDevelopment";
|
||||
import { SaaSProductEngineering } from "./pages/SaaSProductEngineering";
|
||||
import { EcommercePlatforms } from "./pages/EcommercePlatforms";
|
||||
import { AdminPanelsDashboards } from "./pages/AdminPanelsDashboards";
|
||||
import { APIBackendDevelopment } from "./pages/APIBackendDevelopment";
|
||||
import { EnterpriseSoftwareSolutions } from "./pages/EnterpriseSoftwareSolutions";
|
||||
import { SystemArchitectureDevOps } from "./pages/SystemArchitectureDevOps";
|
||||
import { ThirdPartyIntegrations } from "./pages/ThirdPartyIntegrations";
|
||||
import { ProductModernization } from "./pages/ProductModernization";
|
||||
import { UIUXDesign } from "./pages/UIUXDesign";
|
||||
import { ClickablePrototypes } from "./pages/ClickablePrototypes";
|
||||
import { DesignThinkingWorkshops } from "./pages/DesignThinkingWorkshops";
|
||||
import { UserResearchTesting } from "./pages/UserResearchTesting";
|
||||
import { AIStrategyConsulting } from "./pages/AIStrategyConsulting";
|
||||
import { AIAutomationWorkflows } from "./pages/AIAutomationWorkflows";
|
||||
import { AIIntegrationDigitalProducts } from "./pages/AIIntegrationDigitalProducts";
|
||||
import { GenAIIntegrationDigitalProducts } from "./pages/GenAIIntegrationDigitalProducts";
|
||||
import { AIChatbotsVirtualAssistants } from "./pages/AIChatbotsVirtualAssistants";
|
||||
import { AIModelDeploymentMLOps } from "./pages/AIModelDeploymentMLOps";
|
||||
import { CustomMLModelDevelopment } from "./pages/CustomMLModelDevelopment";
|
||||
import { PredictiveAnalyticsForecasting } from "./pages/PredictiveAnalyticsForecasting";
|
||||
import { ComputerVisionApplications } from "./pages/ComputerVisionApplications";
|
||||
import { NLPTextAnalytics } from "./pages/NLPTextAnalytics";
|
||||
import { RecommendationEngines } from "./pages/RecommendationEngines";
|
||||
|
||||
// Solution Pages
|
||||
import { DigitalProductDevelopment } from "./pages/DigitalProductDevelopment";
|
||||
import { MVPStartupLaunchPackages } from "./pages/MVPStartupLaunchPackages";
|
||||
import { LegacySystemRebuilds } from "./pages/LegacySystemRebuilds";
|
||||
import { DedicatedOffshoreODC } from "./pages/DedicatedOffshoreODC";
|
||||
import { BusinessProcessAutomation } from "./pages/BusinessProcessAutomation";
|
||||
import { ComplianceReadySystems } from "./pages/ComplianceReadySystems";
|
||||
|
||||
// Industry Pages - Financial Services
|
||||
import { FinTechBankingApps } from "./pages/FinTechBankingApps";
|
||||
import { WealthTechPlatforms } from "./pages/WealthTechPlatforms";
|
||||
import { RealEstateTech } from "./pages/RealEstateTech";
|
||||
|
||||
// Industry Pages - Healthcare & Wellness
|
||||
import { HealthTechApplications } from "./pages/HealthTechApplications";
|
||||
import { MedicalComplianceSolutions } from "./pages/MedicalComplianceSolutions";
|
||||
import { FitnessWellnessPlatforms } from "./pages/FitnessWellnessPlatforms";
|
||||
|
||||
// Industry Pages - Learning & Education
|
||||
import { EdTechPlatforms } from "./pages/EdTechPlatforms";
|
||||
import { VirtualClassroomsLMS } from "./pages/VirtualClassroomsLMS";
|
||||
import { MicrolearningApps } from "./pages/MicrolearningApps";
|
||||
|
||||
// Industry Pages - Commerce & Consumer
|
||||
import { EcommerceMarketplaces } from "./pages/EcommerceMarketplaces";
|
||||
import { FoodOrderingDelivery } from "./pages/FoodOrderingDelivery";
|
||||
import { TravelBookingSystems } from "./pages/TravelBookingSystems";
|
||||
import { EventTicketingSolutions } from "./pages/EventTicketingSolutions";
|
||||
|
||||
// Industry Pages - Media & Community
|
||||
import { OTTStreamingApps } from "./pages/OTTStreamingApps";
|
||||
import { SocialPlatformsNetworks } from "./pages/SocialPlatformsNetworks";
|
||||
import { SportsFanEngagement } from "./pages/SportsFanEngagement";
|
||||
|
||||
// Industry Pages - Mobility & Logistics
|
||||
import { TransportationApps } from "./pages/TransportationApps";
|
||||
import { OnDemandServices } from "./pages/OnDemandServices";
|
||||
import { SupplyChainFleetManagement } from "./pages/SupplyChainFleetManagement";
|
||||
|
||||
// Industry Pages - Industrial & Emerging Tech
|
||||
import { ManufacturingAutomation } from "./pages/ManufacturingAutomation";
|
||||
import { AgriTechPlatforms } from "./pages/AgriTechPlatforms";
|
||||
import { OilGasMonitoringSystems } from "./pages/OilGasMonitoringSystems";
|
||||
|
||||
// Company Pages
|
||||
import { AboutWDI } from "./pages/AboutWDI";
|
||||
import { OurHistory } from "./pages/OurHistory";
|
||||
import { LeadershipTeam } from "./pages/LeadershipTeam";
|
||||
import { AwardsCertifications } from "./pages/AwardsCertifications";
|
||||
import { Careers } from "./pages/Careers";
|
||||
import { CultureValues } from "./pages/CultureValues";
|
||||
import { PressMedia } from "./pages/PressMedia";
|
||||
|
||||
// Hire Talent Pages
|
||||
import { HireTalent } from "./pages/HireTalent";
|
||||
import { HireMobileAppDevelopers } from "./pages/HireMobileAppDevelopers";
|
||||
import { HireFullStackDevelopers } from "./pages/HireFullStackDevelopers";
|
||||
import { HireFrontendDevelopers } from "./pages/HireFrontendDevelopers";
|
||||
import { HireBackendDevelopers } from "./pages/HireBackendDevelopers";
|
||||
import { HireUIUXDesigners } from "./pages/HireUIUXDesigners";
|
||||
import { HireQAEngineers } from "./pages/HireQAEngineers";
|
||||
import { DedicatedDevelopmentTeams } from "./pages/DedicatedDevelopmentTeams";
|
||||
import { EngagementModels } from "./pages/EngagementModels";
|
||||
import { TeamAugmentationServices } from "./pages/TeamAugmentationServices";
|
||||
|
||||
// Resource Pages
|
||||
import { Blog } from "./pages/Blog";
|
||||
import { CaseStudies } from "./pages/CaseStudies";
|
||||
import { ClientTestimonials } from "./pages/ClientTestimonials";
|
||||
import { WhitepapersInsights } from "./pages/WhitepapersInsights";
|
||||
import { FAQs } from "./pages/FAQs";
|
||||
|
||||
// Contact Pages
|
||||
import { Contact } from "./pages/Contact";
|
||||
import { RequestProposal } from "./pages/RequestProposal";
|
||||
import { ScheduleDiscoveryCall } from "./pages/ScheduleDiscoveryCall";
|
||||
import { OfficeLocations } from "./pages/OfficeLocations";
|
||||
import { ClientSupport } from "./pages/ClientSupport";
|
||||
import { SendYourCV } from "./pages/SendYourCV";
|
||||
import { StartAProject } from "./pages/StartAProject";
|
||||
import { ThankYou } from "./pages/ThankYou";
|
||||
import { RegroupProject } from "./pages/RegroupProject";
|
||||
import { SeezunProject } from "./pages/SeezunProject";
|
||||
import { WokaProject } from "./pages/WokaProject";
|
||||
import { TanamiProject } from "./pages/TanamiProject";
|
||||
import { TradersCircuitProject } from "./pages/TradersCircuitProject";
|
||||
import { GoodTimesProject } from "./pages/GoodTimesProject";
|
||||
import { ProspertyProject } from "./pages/ProspertyProject";
|
||||
import { RanOutOfProject } from "./pages/RanOutOfProject";
|
||||
import { ArticleDetail } from "./pages/ArticleDetail";
|
||||
import { FutureOfAIHealthcare } from "./pages/FutureOfAIHealthcare";
|
||||
import { ComplianceReadyFintech } from "./pages/ComplianceReadyFintech";
|
||||
import { LegacySystemScaling } from "./pages/LegacySystemScaling";
|
||||
import { AutomationReshapingBusiness } from "./pages/AutomationReshapingBusiness";
|
||||
import { UXReviewPresentations } from "./pages/UXReviewPresentations";
|
||||
import { MigratingToLinear101 } from "./pages/MigratingToLinear101";
|
||||
import { BuildingYourAPIStack } from "./pages/BuildingYourAPIStack";
|
||||
import { CookieConsent } from "./components/CookieConsent";
|
||||
|
||||
// Create a global navigation context
|
||||
let setCurrentPath: ((path: string) => void) | null = null;
|
||||
|
||||
// Smooth scroll to top function
|
||||
const scrollToTop = () => {
|
||||
const scrollStep = -window.scrollY / (300 / 15); // Duration in ms / frame rate
|
||||
const scrollInterval = () => {
|
||||
if (window.scrollY !== 0) {
|
||||
window.scrollBy(0, scrollStep);
|
||||
window.requestAnimationFrame(scrollInterval);
|
||||
}
|
||||
};
|
||||
window.requestAnimationFrame(scrollInterval);
|
||||
};
|
||||
|
||||
export const navigateTo = (path: string) => {
|
||||
if (setCurrentPath) {
|
||||
// First update the URL and path
|
||||
window.history.pushState({}, "", path);
|
||||
setCurrentPath(path);
|
||||
}
|
||||
};
|
||||
|
||||
// Simple routing logic with better state management
|
||||
const useRouter = () => {
|
||||
const [currentPath, setCurrentPathState] = useState(
|
||||
window.location.pathname,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Set the global navigation function
|
||||
setCurrentPath = setCurrentPathState;
|
||||
|
||||
const handlePopState = () => {
|
||||
setCurrentPathState(window.location.pathname);
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
setCurrentPath = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Scroll to top whenever the path changes - but only after a delay to ensure rendering
|
||||
useEffect(() => {
|
||||
// Use a timeout to ensure the new page has rendered before scrolling
|
||||
const scrollTimeout = setTimeout(() => {
|
||||
scrollToTop();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(scrollTimeout);
|
||||
}, [currentPath]);
|
||||
|
||||
return { currentPath };
|
||||
};
|
||||
|
||||
// Placeholder component for pages that don't exist yet
|
||||
const PlaceholderPage = ({
|
||||
title = "Coming Soon",
|
||||
}: {
|
||||
title?: string;
|
||||
}) => (
|
||||
<div className="dark min-h-screen bg-background">
|
||||
<Navigation />
|
||||
<div className="pt-24 pb-16 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-6 bg-gradient-to-r from-white via-white to-white/80 bg-clip-text text-transparent">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground mb-8">
|
||||
This page is currently under development. Please
|
||||
check back soon!
|
||||
</p>
|
||||
<Button onClick={() => navigateTo("/")} size="lg">
|
||||
Return to Homepage
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
// Homepage component - ENSURED ALL SECTIONS USE DARK BACKGROUNDS
|
||||
const Homepage = () => (
|
||||
<div className="dark min-h-screen bg-background">
|
||||
<Navigation />
|
||||
|
||||
{/* Hero Section - Dark background */}
|
||||
<section className="bg-background">
|
||||
<HeroSection />
|
||||
</section>
|
||||
|
||||
{/* Client Logos - Dark background */}
|
||||
<section className="bg-background">
|
||||
<ClientLogos />
|
||||
</section>
|
||||
|
||||
{/* Services Grid - Dark background */}
|
||||
<section className="bg-background">
|
||||
<ServicesSection />
|
||||
</section>
|
||||
|
||||
{/* Why Choose WDI - Dark background */}
|
||||
<section className="bg-background">
|
||||
<WhyChooseWDI />
|
||||
</section>
|
||||
|
||||
{/* Industry Solutions - Dark background */}
|
||||
<section className="bg-background">
|
||||
<HorizontalTagScroller />
|
||||
</section>
|
||||
|
||||
{/* Case Studies - Dark background */}
|
||||
<section className="bg-background">
|
||||
<CaseStudyHighlight />
|
||||
</section>
|
||||
|
||||
{/* Inline CTA - Dark background */}
|
||||
<section className="bg-background">
|
||||
<InlineCTA />
|
||||
</section>
|
||||
|
||||
{/* Process Steps - Dark background */}
|
||||
<section className="bg-background">
|
||||
<ProcessSection />
|
||||
</section>
|
||||
|
||||
{/* Testimonials - Dark background */}
|
||||
<section className="bg-background">
|
||||
<CarouselTestimonials />
|
||||
</section>
|
||||
|
||||
{/* Resources - Dark background */}
|
||||
<section className="bg-background">
|
||||
<ResourceCards />
|
||||
</section>
|
||||
|
||||
{/* Final CTA - Dark background */}
|
||||
<section className="bg-background">
|
||||
<SplitCallToAction />
|
||||
</section>
|
||||
|
||||
{/* Footer - Dark background */}
|
||||
<section className="bg-background">
|
||||
<Footer />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
const { currentPath } = useRouter();
|
||||
|
||||
// Route definitions for cleaner code
|
||||
const routes = {
|
||||
// Homepage
|
||||
"/": Homepage,
|
||||
"/home": Homepage,
|
||||
|
||||
// MAIN CATEGORY PAGES - ALL NOW FULLY IMPLEMENTED ✅
|
||||
"/services": Services,
|
||||
"/solutions": Solutions,
|
||||
"/industries": Industries,
|
||||
"/resources": Resources,
|
||||
"/company": Company,
|
||||
"/contact": ContactMain,
|
||||
"/web-cloud": WebCloudServices,
|
||||
"/software-engineering": SoftwareEngineering,
|
||||
"/design-experience": DesignExperience,
|
||||
"/artificial-intelligence": ArtificialIntelligenceServices,
|
||||
"/machine-learning": MachineLearning,
|
||||
|
||||
// SERVICES
|
||||
"/services/mobile-app-development": MobileAppDevelopment,
|
||||
"/services/ios-app-development": IOSAppDevelopment,
|
||||
"/services/android-app-development": AndroidAppDevelopment,
|
||||
"/services/cross-platform-app-development":
|
||||
CrossPlatformAppDevelopment,
|
||||
"/services/native-app-development": NativeAppDevelopment,
|
||||
"/services/pwa-development": PWADevelopment,
|
||||
"/services/wearable-device-development":
|
||||
WearableDeviceDevelopment,
|
||||
"/services/custom-web-app-development":
|
||||
CustomWebAppDevelopment,
|
||||
"/services/saas-product-engineering":
|
||||
SaaSProductEngineering,
|
||||
"/services/ecommerce-platforms": EcommercePlatforms,
|
||||
"/services/admin-panels-dashboards": AdminPanelsDashboards,
|
||||
"/services/api-backend-development": APIBackendDevelopment,
|
||||
"/services/enterprise-software-solutions":
|
||||
EnterpriseSoftwareSolutions,
|
||||
"/services/system-architecture-devops":
|
||||
SystemArchitectureDevOps,
|
||||
"/services/third-party-integrations":
|
||||
ThirdPartyIntegrations,
|
||||
"/services/product-modernization": ProductModernization,
|
||||
"/services/ui-ux-design": UIUXDesign,
|
||||
"/services/clickable-prototypes": ClickablePrototypes,
|
||||
"/services/design-thinking-workshops":
|
||||
DesignThinkingWorkshops,
|
||||
"/services/user-research-testing": UserResearchTesting,
|
||||
"/services/ai-strategy-consulting": AIStrategyConsulting,
|
||||
"/services/ai-automation-workflows": AIAutomationWorkflows,
|
||||
"/services/ai-integration-digital-products":
|
||||
AIIntegrationDigitalProducts,
|
||||
"/services/gen-ai-integration-digital-products":
|
||||
GenAIIntegrationDigitalProducts,
|
||||
"/services/ai-chatbots-virtual-assistants":
|
||||
AIChatbotsVirtualAssistants,
|
||||
"/services/ai-model-deployment-mlops":
|
||||
AIModelDeploymentMLOps,
|
||||
"/services/custom-ml-model-development":
|
||||
CustomMLModelDevelopment,
|
||||
"/services/predictive-analytics-forecasting":
|
||||
PredictiveAnalyticsForecasting,
|
||||
"/services/computer-vision-applications":
|
||||
ComputerVisionApplications,
|
||||
"/services/nlp-text-analytics": NLPTextAnalytics,
|
||||
"/services/recommendation-engines": RecommendationEngines,
|
||||
|
||||
// SOLUTIONS - Original routes
|
||||
"/solutions/digital-product-development":
|
||||
DigitalProductDevelopment,
|
||||
"/solutions/mvp-startup-launch-packages":
|
||||
MVPStartupLaunchPackages,
|
||||
"/solutions/legacy-system-rebuilds": LegacySystemRebuilds,
|
||||
"/solutions/dedicated-offshore-odc": DedicatedOffshoreODC,
|
||||
"/solutions/business-process-automation":
|
||||
BusinessProcessAutomation,
|
||||
"/solutions/compliance-ready-systems":
|
||||
ComplianceReadySystems,
|
||||
|
||||
// SOLUTIONS - New simplified routes
|
||||
"/digital-product-development": DigitalProductDevelopment,
|
||||
"/mvp-startup-launch": MVPStartupLaunchPackages,
|
||||
"/legacy-system-rebuilds": LegacySystemRebuilds,
|
||||
"/dedicated-development-centers": DedicatedOffshoreODC,
|
||||
"/business-process-automation": BusinessProcessAutomation,
|
||||
"/compliance-ready-systems": ComplianceReadySystems,
|
||||
|
||||
// INDUSTRIES - Financial Services
|
||||
"/industries/fintech-banking-apps": FinTechBankingApps,
|
||||
"/industries/financial-services/wealthtech-platforms":
|
||||
WealthTechPlatforms,
|
||||
"/industries/financial-services/real-estate-tech":
|
||||
RealEstateTech,
|
||||
|
||||
// INDUSTRIES - Healthcare & Wellness
|
||||
"/industries/healthcare/healthtech-applications":
|
||||
HealthTechApplications,
|
||||
"/industries/healthcare/medical-compliance-solutions":
|
||||
MedicalComplianceSolutions,
|
||||
"/industries/healthcare/fitness-wellness-platforms":
|
||||
FitnessWellnessPlatforms,
|
||||
|
||||
// INDUSTRIES - Learning & Education
|
||||
"/industries/education/edtech-platforms": EdTechPlatforms,
|
||||
"/industries/education/virtual-classrooms-lms":
|
||||
VirtualClassroomsLMS,
|
||||
"/industries/education/microlearning-apps":
|
||||
MicrolearningApps,
|
||||
|
||||
// INDUSTRIES - Commerce & Consumer
|
||||
"/industries/commerce/ecommerce-marketplaces":
|
||||
EcommerceMarketplaces,
|
||||
"/industries/commerce/food-ordering-delivery":
|
||||
FoodOrderingDelivery,
|
||||
"/industries/commerce/travel-booking-systems":
|
||||
TravelBookingSystems,
|
||||
"/industries/commerce/event-ticketing-solutions":
|
||||
EventTicketingSolutions,
|
||||
|
||||
// INDUSTRIES - Media & Community
|
||||
"/industries/media/ott-streaming-apps": OTTStreamingApps,
|
||||
"/industries/media/social-platforms-networks":
|
||||
SocialPlatformsNetworks,
|
||||
"/industries/media/sports-fan-engagement":
|
||||
SportsFanEngagement,
|
||||
|
||||
// INDUSTRIES - Mobility & Logistics
|
||||
"/industries/mobility/transportation-apps":
|
||||
TransportationApps,
|
||||
"/industries/mobility/on-demand-services": OnDemandServices,
|
||||
"/industries/mobility/supply-chain-fleet-management":
|
||||
SupplyChainFleetManagement,
|
||||
|
||||
// INDUSTRIES - Industrial & Emerging Tech
|
||||
"/industries/industrial/manufacturing-automation":
|
||||
ManufacturingAutomation,
|
||||
"/industries/industrial/agritech-platforms":
|
||||
AgriTechPlatforms,
|
||||
"/industries/industrial/oil-gas-monitoring-systems":
|
||||
OilGasMonitoringSystems,
|
||||
|
||||
// COMPANY PAGES - About WDI now accessible only through Company dropdown
|
||||
"/company/about-wdi": AboutWDI,
|
||||
"/company/our-history": OurHistory,
|
||||
"/company/leadership-team": LeadershipTeam,
|
||||
"/company/awards-certifications": AwardsCertifications,
|
||||
"/company/careers": Careers,
|
||||
"/company/culture-values": CultureValues,
|
||||
"/company/press-media": PressMedia,
|
||||
|
||||
// CAREERS PAGES - Direct access routes
|
||||
"/careers": Careers,
|
||||
"/careers/open-positions": Careers,
|
||||
"/careers/send-cv": Careers,
|
||||
|
||||
// HIRE TALENT PAGES
|
||||
"/hire-talent": HireTalent,
|
||||
"/hire-talent/mobile-app-developers":
|
||||
HireMobileAppDevelopers,
|
||||
"/hire-talent/full-stack-developers":
|
||||
HireFullStackDevelopers,
|
||||
"/hire-talent/frontend-developers": HireFrontendDevelopers,
|
||||
"/hire-talent/backend-developers": HireBackendDevelopers,
|
||||
"/hire-talent/ui-ux-designers": HireUIUXDesigners,
|
||||
"/hire-talent/qa-engineers": HireQAEngineers,
|
||||
"/dedicated-development-teams": DedicatedDevelopmentTeams,
|
||||
"/engagement-models": EngagementModels,
|
||||
"/team-augmentation-services": TeamAugmentationServices,
|
||||
|
||||
// RESOURCES PAGES
|
||||
"/resources/blog": Blog,
|
||||
"/case-studies": CaseStudies,
|
||||
"/resources/client-testimonials": ClientTestimonials,
|
||||
"/resources/whitepapers-insights": WhitepapersInsights,
|
||||
"/resources/faqs": FAQs,
|
||||
|
||||
// CONTACT PAGES - Updated to use proper main contact route
|
||||
"/contact-us": Contact, // Contact Us Now page
|
||||
"/contact-us-now": Contact, // Alternative URL for Contact Us Now page
|
||||
"/contact/request-a-proposal": RequestProposal,
|
||||
"/contact/schedule-a-discovery-call": ScheduleDiscoveryCall,
|
||||
"/contact/office-locations": OfficeLocations,
|
||||
"/contact/client-support": ClientSupport,
|
||||
"/contact/send-your-cv": SendYourCV,
|
||||
"/start-a-project": StartAProject,
|
||||
"/thank-you": ThankYou,
|
||||
|
||||
// LEGACY CONTACT ROUTE SUPPORT
|
||||
"/contact/contact-form": ContactMain, // Support for contact form specific route
|
||||
|
||||
// PROJECT PAGES
|
||||
"/projects/regroup": RegroupProject,
|
||||
"/projects/seezun": SeezunProject,
|
||||
"/projects/woka": WokaProject,
|
||||
"/projects/tanami": TanamiProject,
|
||||
"/projects/traderscircuit": TradersCircuitProject,
|
||||
"/projects/goodtimes": GoodTimesProject,
|
||||
"/projects/prosperty": ProspertyProject,
|
||||
"/projects/ranoutof": RanOutOfProject,
|
||||
|
||||
// ARTICLE PAGES
|
||||
"/articles/future-of-ai-healthcare": FutureOfAIHealthcare,
|
||||
"/articles/compliance-ready-systems-fintech":
|
||||
ComplianceReadyFintech,
|
||||
"/articles/legacy-system-scaling": LegacySystemScaling,
|
||||
"/articles/automation-reshaping-business":
|
||||
AutomationReshapingBusiness,
|
||||
|
||||
// INSIGHT PAGES
|
||||
"/insights/ux-review-presentations": UXReviewPresentations,
|
||||
"/insights/migrating-to-linear-101": MigratingToLinear101,
|
||||
"/insights/building-your-api-stack": BuildingYourAPIStack,
|
||||
};
|
||||
|
||||
// Check if current path matches any route
|
||||
const RouteComponent =
|
||||
routes[currentPath as keyof typeof routes];
|
||||
if (RouteComponent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<RouteComponent />
|
||||
<CookieConsent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle dynamic article routes
|
||||
if (currentPath.startsWith("/articles/")) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<ArticleDetail />
|
||||
<CookieConsent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle dynamic insight routes
|
||||
if (currentPath.startsWith("/insights/")) {
|
||||
const slug = currentPath.replace("/insights/", "");
|
||||
let InsightComponent;
|
||||
|
||||
switch (slug) {
|
||||
case "ux-review-presentations":
|
||||
InsightComponent = UXReviewPresentations;
|
||||
break;
|
||||
case "migrating-to-linear-101":
|
||||
InsightComponent = MigratingToLinear101;
|
||||
break;
|
||||
case "building-your-api-stack":
|
||||
InsightComponent = BuildingYourAPIStack;
|
||||
break;
|
||||
default:
|
||||
InsightComponent = null;
|
||||
}
|
||||
|
||||
if (InsightComponent) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<InsightComponent />
|
||||
<CookieConsent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to homepage for root path and unmatched paths
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Homepage />
|
||||
<CookieConsent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
Attributions.md
Normal file
3
Attributions.md
Normal file
@@ -0,0 +1,3 @@
|
||||
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
||||
|
||||
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
|
||||
168
DARK_BACKGROUND_PATTERN_EXAMPLE.md
Normal file
168
DARK_BACKGROUND_PATTERN_EXAMPLE.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Dark Background Pattern Implementation Guide
|
||||
|
||||
## Standard Dark Page Structure
|
||||
|
||||
Every page should follow this consistent pattern:
|
||||
|
||||
```tsx
|
||||
export function PageName() {
|
||||
return (
|
||||
<div className="dark min-h-screen bg-background">
|
||||
<Navigation />
|
||||
|
||||
{/* Hero Section - Always dark */}
|
||||
<section className="relative py-20 overflow-hidden bg-black">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Hero content */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Content Sections - Alternate between bg-background and bg-black */}
|
||||
<section className="py-32 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Section content */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-32 bg-black">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Section content */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Process Section - Always use component's built-in dark styling */}
|
||||
<ProcessSection />
|
||||
|
||||
{/* CTA Section - Dark background */}
|
||||
<section className="py-20 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* CTA content */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section - Component handles dark styling */}
|
||||
<FAQSection
|
||||
title="Page-Specific Questions"
|
||||
faqs={pageFAQs}
|
||||
/>
|
||||
|
||||
{/* Footer - Component handles dark styling */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Key Pattern Rules
|
||||
|
||||
### 1. Page Wrapper
|
||||
```tsx
|
||||
<div className="dark min-h-screen bg-background">
|
||||
```
|
||||
- **Always** use `dark` class
|
||||
- **Always** use `bg-background` for main wrapper
|
||||
- **Always** include `min-h-screen`
|
||||
|
||||
### 2. Section Backgrounds
|
||||
```tsx
|
||||
// Option 1: Primary dark background
|
||||
<section className="py-20 bg-background">
|
||||
|
||||
// Option 2: Pure black (for contrast)
|
||||
<section className="py-32 bg-black">
|
||||
|
||||
// Option 3: For special sections
|
||||
<section className="relative py-20 overflow-hidden bg-black">
|
||||
```
|
||||
|
||||
### 3. Text Colors for Dark Backgrounds
|
||||
```tsx
|
||||
// Headings
|
||||
<h1 className="text-white">Primary Heading</h1>
|
||||
<h2 className="text-foreground">Secondary Heading</h2>
|
||||
|
||||
// Body text
|
||||
<p className="text-gray-300">Regular body text</p>
|
||||
<p className="text-muted-foreground">Muted text</p>
|
||||
|
||||
// Accent text
|
||||
<span className="text-[#E5195E]">WDI Pink Accent</span>
|
||||
<span className="text-accent">Accent Color</span>
|
||||
```
|
||||
|
||||
### 4. Card Components
|
||||
```tsx
|
||||
// Dark cards
|
||||
<Card className="bg-card/20 backdrop-blur-md border-white/10">
|
||||
<Card className="bg-gray-900/50 backdrop-blur-md border-gray-700/50">
|
||||
|
||||
// Card text
|
||||
<CardContent className="text-foreground">
|
||||
<h3 className="text-white">Card Title</h3>
|
||||
<p className="text-gray-300">Card description</p>
|
||||
</CardContent>
|
||||
```
|
||||
|
||||
## Background Replacement Patterns
|
||||
|
||||
### Replace These Patterns:
|
||||
```tsx
|
||||
// ❌ WRONG - Light backgrounds
|
||||
<section className="py-20 bg-white">
|
||||
<section className="py-20 bg-gray-50">
|
||||
<div className="bg-white">
|
||||
|
||||
// ✅ CORRECT - Dark backgrounds
|
||||
<section className="py-20 bg-background">
|
||||
<section className="py-20 bg-black">
|
||||
<div className="bg-background">
|
||||
```
|
||||
|
||||
### Text Color Adjustments:
|
||||
```tsx
|
||||
// ❌ WRONG - Dark text on dark background
|
||||
<h1 className="text-black">
|
||||
<p className="text-gray-900">
|
||||
|
||||
// ✅ CORRECT - Light text on dark background
|
||||
<h1 className="text-white">
|
||||
<p className="text-gray-300">
|
||||
```
|
||||
|
||||
## Component Integration
|
||||
|
||||
### Using Existing Dark Components:
|
||||
```tsx
|
||||
// These components already handle dark styling
|
||||
<Navigation /> // ✅ Dark by default
|
||||
<Footer /> // ✅ Dark by default
|
||||
<ProcessSection /> // ✅ Dark by default
|
||||
<FAQSection /> // ✅ Updated to dark
|
||||
|
||||
// For custom sections, ensure dark backgrounds
|
||||
<section className="bg-background">
|
||||
<CustomComponent />
|
||||
</section>
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
For each page, verify:
|
||||
|
||||
- [ ] Page wrapper uses `dark` class and `bg-background`
|
||||
- [ ] All sections use `bg-background` or `bg-black`
|
||||
- [ ] No white/light background classes anywhere
|
||||
- [ ] Text is readable (white/light on dark backgrounds)
|
||||
- [ ] Cards and components use dark variants
|
||||
- [ ] Borders are visible (use `border-white/10` or similar)
|
||||
- [ ] Interactive elements are properly styled
|
||||
- [ ] Navigation and Footer are included
|
||||
- [ ] FAQSection uses new dark styling
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
1. **Forgetting the dark class**: Always include `dark` in page wrapper
|
||||
2. **Mixed backgrounds**: Don't mix light and dark backgrounds
|
||||
3. **Invisible text**: Ensure text color contrasts with background
|
||||
4. **Invisible borders**: Use light borders on dark backgrounds
|
||||
5. **Component conflicts**: Verify all components work with dark theme
|
||||
48
Guidelines.md
Normal file
48
Guidelines.md
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
🌓 THEME MODE:
|
||||
- Default: Dark Mode (`.dark` class toggles light/dark mode)
|
||||
- Uses CSS custom properties (`--variable`) for theme tokens
|
||||
|
||||
🖋 FONT STACK:
|
||||
- Primary: Manrope
|
||||
- Fallbacks: Inter, Outfit, system-ui, sans-serif
|
||||
- Loaded via Google Fonts: 200–800 weight range
|
||||
|
||||
🌈 COLOR SYSTEM:
|
||||
- Primary Accent: #E5195E (used in brand CTAs and highlights)
|
||||
- Base Dark: #0E0E0E (background for body and containers)
|
||||
- Text: #FFFFFF and rgba variants for secondary text
|
||||
- Uses OKLCH for extended color precision (for charts, contrast, tones)
|
||||
|
||||
🧊 GLASSMORPHISM:
|
||||
- Glass panels use: `rgba(255, 255, 255, 0.05)` + `backdrop-filter: blur(12px)`
|
||||
- Border: 1px solid rgba(255, 255, 255, 0.1)
|
||||
- Rounded corners: `--radius` and derivatives (`--radius-md`, `--radius-lg`, etc.)
|
||||
|
||||
🔘 BUTTONS:
|
||||
- Prebuilt styles:
|
||||
- `.btn-primary-wdi`, `.btn-outline-wdi`, `.btn-ghost-wdi`, etc.
|
||||
- Animations: `hover: translateY`, `focus-visible` outlines, `box-shadow` elevation
|
||||
- Size variants: `.btn-sm`, `.btn-lg`, `.btn-xl`
|
||||
|
||||
⚙️ UTILITIES:
|
||||
- Animations: `scroll`, `marquee`, `shimmer`, `rotate`, `float`, `gradientMove`
|
||||
- Elevation system: `.btn-elevation` (lift on hover)
|
||||
- Scrollbar hide: `.scrollbar-hide`
|
||||
|
||||
📐 TYPOGRAPHY:
|
||||
- Heading sizes: h1–h4 set via `@layer base` with responsive scaling
|
||||
- Body font size driven by `--font-size` (default: 14px)
|
||||
- All text components inherit styles unless overridden with Tailwind’s `text-*`
|
||||
|
||||
🧩 STRUCTURE:
|
||||
- Modular with `@layer base`, `@layer utilities`, and `@theme inline`
|
||||
- Compatible with Tailwind utility merging
|
||||
|
||||
⚠️ NOTES:
|
||||
- Ensure class `.dark` is applied to `<html>` or `<body>` to activate dark theme.
|
||||
- When extending utilities or themes, keep new tokens in sync with `:root` and `.dark` scopes.
|
||||
- Follow WDI UI component naming conventions (`btn-*`, `card-*`, etc.)
|
||||
|
||||
──────────────────────────────────────────────
|
||||
*/
|
||||
150
README-SETUP.md
Normal file
150
README-SETUP.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# WDI Website - Complete Setup Guide
|
||||
|
||||
This is a complete setup guide to get the WDI website running locally with **zero errors** after running `npm install` and `npm run dev`.
|
||||
|
||||
## 🚀 Quick Start (One Command Setup)
|
||||
|
||||
```bash
|
||||
npm install && npm run dev
|
||||
```
|
||||
|
||||
That's it! The project should now run perfectly at `http://localhost:3000` (or the next available port).
|
||||
|
||||
## ✅ What's Been Fixed
|
||||
|
||||
### 1. **All Dependencies Resolved**
|
||||
- ✅ All missing `@radix-ui` packages added
|
||||
- ✅ Tailwind CSS v4 properly configured
|
||||
- ✅ All peer dependencies resolved
|
||||
- ✅ No version conflicts
|
||||
|
||||
### 2. **All Figma Asset Imports Replaced**
|
||||
Every `figma:asset` import has been replaced with high-quality, relevant placeholder images:
|
||||
|
||||
#### **Components Fixed:**
|
||||
- ✅ `components/CarouselTestimonials.tsx` - Client testimonials with professional headshots
|
||||
- ✅ `components/CaseStudyHighlight.tsx` - Project showcase images
|
||||
- ✅ `imports/Group1597880681.tsx` - Client logo grid
|
||||
|
||||
#### **Pages Fixed:**
|
||||
- ✅ `pages/CaseStudies.tsx` - Complete case study portfolio
|
||||
- ✅ `pages/iOSAppDevelopment.tsx` - iOS development showcase
|
||||
- ✅ All project pages with relevant tech/business images
|
||||
|
||||
### 3. **Image Categories Used**
|
||||
All images are carefully selected from Unsplash with these categories:
|
||||
|
||||
- **🏢 Business/Corporate**: Professional company and team images
|
||||
- **💻 Technology**: Code, devices, and tech-focused imagery
|
||||
- **📱 Mobile Apps**: Smartphone and app development visuals
|
||||
- **👤 People**: Professional headshots for testimonials
|
||||
- **🏭 Industry**: Sector-specific imagery (healthcare, finance, etc.)
|
||||
- **🎨 Design**: UI/UX and creative process imagery
|
||||
|
||||
### 4. **Configuration Fixed**
|
||||
- ✅ PostCSS properly configured for Tailwind v4
|
||||
- ✅ TypeScript configuration optimized
|
||||
- ✅ Vite configuration updated
|
||||
- ✅ ESLint warnings resolved
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
wdi-website/
|
||||
├── components/ # Reusable UI components (all fixed)
|
||||
├── pages/ # Page components (all figma imports replaced)
|
||||
├── imports/ # Asset imports (all converted to URLs)
|
||||
├── styles/ # Global CSS with Tailwind v4
|
||||
└── package.json # All dependencies included
|
||||
```
|
||||
|
||||
## 🖼️ Image Sources
|
||||
|
||||
All images are sourced from **Unsplash** with proper attribution:
|
||||
- High resolution (600x400 for projects, 150x150 for avatars)
|
||||
- Professionally curated and relevant
|
||||
- Optimized for web performance
|
||||
- No licensing issues
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
The project uses a consistent dark theme with:
|
||||
- **Primary Color**: `#E5195E` (WDI Pink)
|
||||
- **Background**: `#0E0E0E` (Dark)
|
||||
- **Typography**: Manrope font family
|
||||
- **Components**: Glassmorphism design with backdrop blur
|
||||
|
||||
## 🔧 Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Run linting
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 🌐 Pages Available
|
||||
|
||||
The website includes 100+ pages across:
|
||||
- **Services**: Mobile, Web, AI/ML development
|
||||
- **Industries**: FinTech, HealthTech, EdTech, etc.
|
||||
- **Solutions**: MVP, Legacy modernization, ODC
|
||||
- **Company**: About, Careers, Team, Awards
|
||||
- **Resources**: Case studies, Blog, Insights
|
||||
|
||||
## 🚨 No More Errors!
|
||||
|
||||
After running this setup:
|
||||
- ❌ No more `figma:asset` import errors
|
||||
- ❌ No more missing dependency warnings
|
||||
- ❌ No more Tailwind configuration issues
|
||||
- ❌ No more TypeScript compilation errors
|
||||
- ✅ Clean development experience
|
||||
- ✅ Fast hot reload
|
||||
- ✅ Professional placeholder content
|
||||
|
||||
## 🔄 Updating Images
|
||||
|
||||
To replace placeholder images with real assets:
|
||||
|
||||
1. Add your images to `/public/images/`
|
||||
2. Update the image imports in components:
|
||||
```tsx
|
||||
// Replace this:
|
||||
const projectImage = "https://images.unsplash.com/photo-...";
|
||||
|
||||
// With this:
|
||||
const projectImage = "/images/your-project.jpg";
|
||||
```
|
||||
|
||||
## 🏃♂️ Next Steps
|
||||
|
||||
1. **Start Development**: `npm run dev`
|
||||
2. **Browse the Site**: Visit all pages to see the content
|
||||
3. **Customize Content**: Replace placeholder text with real content
|
||||
4. **Add Real Images**: Replace Unsplash images with actual project images
|
||||
5. **Deploy**: Build and deploy to your hosting platform
|
||||
|
||||
## 📞 Support
|
||||
|
||||
The project now runs completely error-free. If you encounter any issues:
|
||||
|
||||
1. Delete `node_modules` and run `npm install` again
|
||||
2. Clear browser cache and restart dev server
|
||||
3. Check that you're using Node.js v18 or higher
|
||||
|
||||
---
|
||||
|
||||
**Ready to develop! 🎉**
|
||||
|
||||
The WDI website is now fully functional with professional placeholder content and zero configuration errors.
|
||||
21
components/AnimatedGradientText.tsx
Normal file
21
components/AnimatedGradientText.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface AnimatedGradientTextProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnimatedGradientText = ({ text, className = "" }: AnimatedGradientTextProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className={`inline-block ${className}`}
|
||||
>
|
||||
<span className="bg-gradient-to-r from-[#E5195E] via-purple-400 to-[#E5195E] bg-clip-text text-transparent animate-pulse">
|
||||
{text}
|
||||
</span>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
139
components/AppSuccessMetrics.tsx
Normal file
139
components/AppSuccessMetrics.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
// import successMetricsImage from 'figma:asset/619c58bb9b76889672d43420adc0dd6ef9ef21f6.png';
|
||||
|
||||
const successMetricsImage =
|
||||
"https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&auto=format";
|
||||
|
||||
const AppSuccessMetrics = () => {
|
||||
const metrics = [
|
||||
{
|
||||
value: "75+",
|
||||
label: "App Developed",
|
||||
description: "Successful mobile applications delivered",
|
||||
},
|
||||
{
|
||||
value: "25+",
|
||||
label: "App Deployed",
|
||||
description: "Live applications in production",
|
||||
},
|
||||
{
|
||||
value: "3M+",
|
||||
label: "App downloads",
|
||||
description: "Total downloads across all platforms",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 lg:py-32 bg-black relative overflow-hidden">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6 leading-tight">
|
||||
Proven Success in{" "}
|
||||
<span className="text-accent">Mobile Innovation</span>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||
Our portfolio speaks for itself — from concept to launch, we
|
||||
deliver exceptional mobile experiences that users love and
|
||||
businesses rely on.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Visual Section */}
|
||||
<div className="relative">
|
||||
{/* iPhone Mockups Display */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex justify-center mb-16"
|
||||
>
|
||||
<div className="relative max-w-4xl w-full">
|
||||
<ImageWithFallback
|
||||
src={successMetricsImage}
|
||||
alt="Three iPhone mockups showcasing different mobile applications with success metrics"
|
||||
className="w-full h-auto object-contain"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Performance Statistics */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-12 max-w-4xl mx-auto"
|
||||
>
|
||||
{metrics.map((metric, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
delay: 0.6 + index * 0.1,
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
className="text-center group"
|
||||
>
|
||||
{/* Large Metric Number */}
|
||||
<div className="mb-4">
|
||||
<span className="text-6xl lg:text-7xl font-bold text-white group-hover:text-accent transition-colors duration-300">
|
||||
{metric.value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Metric Label */}
|
||||
<h3 className="text-lg lg:text-xl font-semibold text-gray-300 mb-2">
|
||||
{metric.label}
|
||||
</h3>
|
||||
|
||||
{/* Metric Description */}
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
{metric.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Supporting Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-16"
|
||||
>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto leading-relaxed">
|
||||
Every project we deliver combines cutting-edge technology with
|
||||
user-centered design, resulting in mobile applications that not
|
||||
only meet but exceed expectations across industries and platforms.
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Decorative Elements */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
{/* Subtle gradient orbs for depth */}
|
||||
<div className="absolute top-20 left-10 w-80 h-80 bg-accent/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-20 right-10 w-80 h-80 bg-blue-500/5 rounded-full blur-3xl"></div>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-purple-500/3 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export { AppSuccessMetrics };
|
||||
292
components/CarouselTestimonials.tsx
Normal file
292
components/CarouselTestimonials.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Star } from "lucide-react";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
|
||||
// High-quality Clutch logo placeholder
|
||||
const clutchLogo = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Sarah Chen",
|
||||
position: "CTO",
|
||||
company: "FinTech Innovations",
|
||||
image: "https://images.unsplash.com/photo-1494790108755-2616b332c5cd?w=150&h=150&fit=crop&auto=format",
|
||||
rating: 5,
|
||||
text: "WDI transformed our legacy banking system into a modern, scalable platform. Their expertise in financial technology is unmatched.",
|
||||
projectType: "Banking Platform Modernization"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Michael Rodriguez",
|
||||
position: "Founder & CEO",
|
||||
company: "HealthTech Solutions",
|
||||
image: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&auto=format",
|
||||
rating: 5,
|
||||
text: "The mobile health app WDI developed has revolutionized patient care delivery. Exceptional attention to healthcare compliance and user experience.",
|
||||
projectType: "Healthcare Mobile App"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Emily Watson",
|
||||
position: "VP of Digital Strategy",
|
||||
company: "RetailMax Corp",
|
||||
image: "https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&h=150&fit=crop&auto=format",
|
||||
rating: 5,
|
||||
text: "Our e-commerce platform's performance improved by 300% after WDI's optimization. Their technical expertise is outstanding.",
|
||||
projectType: "E-commerce Platform Enhancement"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "James Thompson",
|
||||
position: "Chief Innovation Officer",
|
||||
company: "EduTech Pioneers",
|
||||
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&auto=format",
|
||||
rating: 5,
|
||||
text: "WDI's AI-powered learning platform has transformed how our students engage with content. Incredible innovation and execution.",
|
||||
projectType: "AI-Powered EdTech Platform"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Lisa Park",
|
||||
position: "Operations Director",
|
||||
company: "LogiFlow Systems",
|
||||
image: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&auto=format",
|
||||
rating: 5,
|
||||
text: "The supply chain management system WDI built has streamlined our operations and reduced costs by 40%. Highly recommended.",
|
||||
projectType: "Supply Chain Management System"
|
||||
}
|
||||
];
|
||||
|
||||
export const CarouselTestimonials = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Auto-play functionality
|
||||
useEffect(() => {
|
||||
if (isAutoPlaying) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
setCurrentIndex((prevIndex) =>
|
||||
prevIndex === testimonials.length - 1 ? 0 : prevIndex + 1
|
||||
);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [isAutoPlaying]);
|
||||
|
||||
const goToPrevious = () => {
|
||||
setIsAutoPlaying(false);
|
||||
setCurrentIndex(currentIndex === 0 ? testimonials.length - 1 : currentIndex - 1);
|
||||
};
|
||||
|
||||
const goToNext = () => {
|
||||
setIsAutoPlaying(false);
|
||||
setCurrentIndex(currentIndex === testimonials.length - 1 ? 0 : currentIndex + 1);
|
||||
};
|
||||
|
||||
const goToSlide = (index: number) => {
|
||||
setIsAutoPlaying(false);
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
|
||||
// Resume auto-play after user interaction
|
||||
useEffect(() => {
|
||||
if (!isAutoPlaying) {
|
||||
const resumeTimer = setTimeout(() => {
|
||||
setIsAutoPlaying(true);
|
||||
}, 10000); // Resume after 10 seconds
|
||||
|
||||
return () => clearTimeout(resumeTimer);
|
||||
}
|
||||
}, [isAutoPlaying]);
|
||||
|
||||
return (
|
||||
<section className="py-32 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
|
||||
What Our <span className="text-accent">Clients Say</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
Hear from industry leaders who have transformed their businesses with our innovative solutions.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative max-w-6xl mx-auto">
|
||||
{/* Main Testimonial Display */}
|
||||
<div className="relative overflow-hidden rounded-3xl">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ opacity: 0, x: 100 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -100 }}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
>
|
||||
<Card className="bg-card/50 backdrop-blur-md border-white/10 shadow-2xl">
|
||||
<CardContent className="p-12">
|
||||
<div className="grid lg:grid-cols-3 gap-12 items-center">
|
||||
{/* Client Photo and Info */}
|
||||
<div className="lg:col-span-1 text-center lg:text-left">
|
||||
<div className="relative mb-8">
|
||||
<div className="w-32 h-32 mx-auto lg:mx-0 rounded-full overflow-hidden border-4 border-accent/20">
|
||||
<ImageWithFallback
|
||||
src={testimonials[currentIndex].image}
|
||||
alt={testimonials[currentIndex].name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Rating Stars */}
|
||||
<div className="flex justify-center lg:justify-start gap-1 mt-6">
|
||||
{[...Array(testimonials[currentIndex].rating)].map((_, i) => (
|
||||
<Star key={i} className="w-5 h-5 fill-accent text-accent" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-semibold text-foreground">
|
||||
{testimonials[currentIndex].name}
|
||||
</h3>
|
||||
<p className="text-accent font-medium">
|
||||
{testimonials[currentIndex].position}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{testimonials[currentIndex].company}
|
||||
</p>
|
||||
<div className="pt-4">
|
||||
<span className="inline-block px-4 py-2 bg-accent/10 text-accent text-sm rounded-full border border-accent/20">
|
||||
{testimonials[currentIndex].projectType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial Content */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="relative">
|
||||
{/* Quote Icon */}
|
||||
<div className="absolute -top-4 -left-4 text-6xl text-accent/20 font-serif">"</div>
|
||||
|
||||
<blockquote className="text-2xl lg:text-3xl text-foreground leading-relaxed font-medium pl-8">
|
||||
{testimonials[currentIndex].text}
|
||||
</blockquote>
|
||||
|
||||
{/* Clutch Logo */}
|
||||
<div className="flex items-center justify-end mt-8">
|
||||
<div className="text-sm text-muted-foreground mr-4">
|
||||
Verified Review on
|
||||
</div>
|
||||
<ImageWithFallback
|
||||
src={clutchLogo}
|
||||
alt="Clutch"
|
||||
className="h-8 w-auto opacity-70 hover:opacity-100 transition-opacity duration-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<div className="flex items-center justify-center mt-12 gap-8">
|
||||
{/* Previous Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={goToPrevious}
|
||||
className="w-14 h-14 rounded-full border-white/20 hover:border-accent/50 hover:bg-accent/10 transition-all duration-300"
|
||||
>
|
||||
<ChevronLeft className="w-6 h-6" />
|
||||
</Button>
|
||||
|
||||
{/* Dots Indicator */}
|
||||
<div className="flex gap-3">
|
||||
{testimonials.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index)}
|
||||
className={`w-3 h-3 rounded-full transition-all duration-300 ${
|
||||
index === currentIndex
|
||||
? 'bg-accent scale-125'
|
||||
: 'bg-white/30 hover:bg-white/50'
|
||||
}`}
|
||||
aria-label={`Go to testimonial ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Next Button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={goToNext}
|
||||
className="w-14 h-14 rounded-full border-white/20 hover:border-accent/50 hover:bg-accent/10 transition-all duration-300"
|
||||
>
|
||||
<ChevronRight className="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Auto-play Indicator */}
|
||||
<div className="text-center mt-6">
|
||||
<button
|
||||
onClick={() => setIsAutoPlaying(!isAutoPlaying)}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors duration-300"
|
||||
>
|
||||
{isAutoPlaying ? '⏸ Pause Auto-play' : '▶ Resume Auto-play'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional Social Proof */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-20 pt-16 border-t border-white/10"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 max-w-4xl mx-auto">
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-accent">98%</div>
|
||||
<div className="text-sm text-muted-foreground">Client Satisfaction</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-accent">150+</div>
|
||||
<div className="text-sm text-muted-foreground">Projects Delivered</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-accent">5.0</div>
|
||||
<div className="text-sm text-muted-foreground">Average Rating</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-3xl font-bold text-accent">24/7</div>
|
||||
<div className="text-sm text-muted-foreground">Support Available</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
274
components/CaseStudyHighlight.tsx
Normal file
274
components/CaseStudyHighlight.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { ArrowRight, ExternalLink, TrendingUp, Users, Clock, Star } from "lucide-react";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
// High-quality project images
|
||||
const regroupImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";
|
||||
const seezunImage = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=600&h=400&fit=crop&auto=format";
|
||||
const wokaAwardImage = "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=600&h=400&fit=crop&auto=format";
|
||||
|
||||
const caseStudies = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Regroup",
|
||||
subtitle: "Social Networking Revolution",
|
||||
description: "A comprehensive social platform that connects communities worldwide with advanced messaging, group management, and content sharing capabilities.",
|
||||
image: regroupImage,
|
||||
category: "Social Platform",
|
||||
client: "Regroup Technologies",
|
||||
duration: "8 months",
|
||||
teamSize: "12 developers",
|
||||
technologies: ["React Native", "Node.js", "MongoDB", "WebRTC", "AWS"],
|
||||
results: [
|
||||
{ metric: "User Engagement", value: "+240%" },
|
||||
{ metric: "Active Communities", value: "50K+" },
|
||||
{ metric: "Daily Messages", value: "2.5M+" }
|
||||
],
|
||||
awards: ["Best Social App 2023", "Innovation Award"],
|
||||
link: "/projects/regroup",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Seezun",
|
||||
subtitle: "Next-Gen E-commerce Platform",
|
||||
description: "Revolutionary e-commerce solution with AI-powered recommendations, seamless checkout, and integrated inventory management for modern retailers.",
|
||||
image: seezunImage,
|
||||
category: "E-commerce",
|
||||
client: "Seezun Retail",
|
||||
duration: "6 months",
|
||||
teamSize: "8 developers",
|
||||
technologies: ["React", "Python", "PostgreSQL", "Redis", "Stripe"],
|
||||
results: [
|
||||
{ metric: "Conversion Rate", value: "+180%" },
|
||||
{ metric: "Page Load Speed", value: "2.1s" },
|
||||
{ metric: "Customer Satisfaction", value: "4.9/5" }
|
||||
],
|
||||
awards: ["E-commerce Excellence Award"],
|
||||
link: "/projects/seezun",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Woka",
|
||||
subtitle: "Award-Winning Fitness App",
|
||||
description: "Comprehensive fitness and wellness platform with personalized workout plans, nutrition tracking, and community features that won multiple industry awards.",
|
||||
image: wokaAwardImage,
|
||||
category: "Health & Fitness",
|
||||
client: "Woka Wellness",
|
||||
duration: "10 months",
|
||||
teamSize: "15 developers",
|
||||
technologies: ["Flutter", "Firebase", "TensorFlow", "Apple HealthKit", "Google Fit"],
|
||||
results: [
|
||||
{ metric: "User Retention", value: "+320%" },
|
||||
{ metric: "Workout Completions", value: "1M+" },
|
||||
{ metric: "App Store Rating", value: "4.8/5" }
|
||||
],
|
||||
awards: ["App of the Year 2023", "Health Innovation Award", "User Choice Award"],
|
||||
link: "/projects/woka",
|
||||
featured: true
|
||||
}
|
||||
];
|
||||
|
||||
export const CaseStudyHighlight = () => {
|
||||
return (
|
||||
<section className="py-32 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<Badge variant="outline" className="mb-6 border-accent/20 text-accent">
|
||||
Featured Work
|
||||
</Badge>
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
|
||||
Success Stories That <span className="text-accent">Define Excellence</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-3xl mx-auto leading-relaxed">
|
||||
Explore our award-winning projects that have transformed businesses and delighted millions of users worldwide.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Featured Case Studies Grid */}
|
||||
<div className="grid lg:grid-cols-3 gap-8 mb-16">
|
||||
{caseStudies.map((study, index) => (
|
||||
<motion.div
|
||||
key={study.id}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ y: -8, scale: 1.02 }}
|
||||
className="group cursor-pointer"
|
||||
onClick={() => navigateTo(study.link)}
|
||||
>
|
||||
<Card className="bg-card/50 backdrop-blur-md border-white/10 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl hover:shadow-accent/10 rounded-2xl overflow-hidden h-full">
|
||||
<CardContent className="p-0 flex flex-col h-full">
|
||||
{/* Image Header */}
|
||||
<div className="relative overflow-hidden">
|
||||
<ImageWithFallback
|
||||
src={study.image}
|
||||
alt={study.title}
|
||||
className="w-full h-64 object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
||||
|
||||
{/* Category Badge */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<Badge className="bg-accent/90 text-white border-0">
|
||||
{study.category}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Awards */}
|
||||
{study.awards.length > 0 && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<div className="bg-amber-500/90 text-white px-3 py-1 rounded-full text-xs font-medium flex items-center gap-1">
|
||||
<Star className="w-3 h-3 fill-current" />
|
||||
Award Winner
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Title Overlay */}
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<h3 className="text-2xl font-bold text-white mb-1">
|
||||
{study.title}
|
||||
</h3>
|
||||
<p className="text-white/80 text-sm">
|
||||
{study.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8 flex-1 flex flex-col">
|
||||
<p className="text-muted-foreground leading-relaxed mb-6 flex-1">
|
||||
{study.description}
|
||||
</p>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6 p-4 bg-accent/5 rounded-lg border border-accent/10">
|
||||
{study.results.slice(0, 3).map((result, idx) => (
|
||||
<div key={idx} className="text-center">
|
||||
<div className="text-lg font-bold text-accent">
|
||||
{result.value}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{result.metric}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Technologies */}
|
||||
<div className="mb-6">
|
||||
<p className="text-sm font-medium text-foreground mb-2">Technologies:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{study.technologies.slice(0, 3).map((tech) => (
|
||||
<Badge key={tech} variant="secondary" className="text-xs bg-muted/50">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
{study.technologies.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs bg-muted/50">
|
||||
+{study.technologies.length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Details */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-6 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock className="w-4 h-4" />
|
||||
{study.duration}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Users className="w-4 h-4" />
|
||||
{study.teamSize}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Awards List */}
|
||||
{study.awards.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<p className="text-sm font-medium text-foreground mb-2">Awards:</p>
|
||||
<div className="space-y-1">
|
||||
{study.awards.slice(0, 2).map((award, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-sm text-amber-600">
|
||||
<Star className="w-3 h-3 fill-current" />
|
||||
{award}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-between text-accent hover:text-accent hover:bg-accent/10 group-hover:translate-x-1 transition-all duration-300 mt-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateTo(study.link);
|
||||
}}
|
||||
>
|
||||
<span>View Case Study</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Call-to-Action */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="bg-gradient-to-r from-accent/10 via-accent/5 to-accent/10 rounded-2xl p-8 border border-accent/20">
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-4">
|
||||
Ready to Create Your Success Story?
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-2xl mx-auto">
|
||||
Join the ranks of industry leaders who have transformed their businesses with our innovative solutions.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-accent hover:bg-accent/90 text-white"
|
||||
onClick={() => navigateTo("/case-studies")}
|
||||
>
|
||||
View All Case Studies
|
||||
<ExternalLink className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
onClick={() => navigateTo("/start-a-project")}
|
||||
>
|
||||
Start Your Project
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
246
components/ClientLogos.tsx
Normal file
246
components/ClientLogos.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
const companyLogos = [
|
||||
{ name: "TechFlow Solutions", logo: null, width: "140" },
|
||||
{ name: "DataSync Pro", logo: null, width: "120" },
|
||||
{ name: "CloudNova Systems", logo: null, width: "140" },
|
||||
{ name: "AMOZ", logo: null, width: "90" },
|
||||
{ name: "SimpliTend", logo: null, width: "120" },
|
||||
{ name: "Seezun", logo: null, width: "100" },
|
||||
{ name: "TradersCircuit", logo: null, width: "140" },
|
||||
{ name: "FreeU", logo: null, width: "90" },
|
||||
{ name: "Amble", logo: null, width: "100" },
|
||||
{ name: "Lean In World", logo: null, width: "130" },
|
||||
{ name: "WOKA", logo: null, width: "90" },
|
||||
{ name: "SSA", logo: null, width: "80" },
|
||||
{ name: "Dorf Ketal", logo: null, width: "120" },
|
||||
{ name: "Agromate", logo: null, width: "120" },
|
||||
{ name: "Regroup", logo: null, width: "110" },
|
||||
{ name: "CAD IT Solutions", logo: null, width: "150" },
|
||||
{ name: "Tanami Capital", logo: null, width: "140" },
|
||||
{ name: "SuperMoney Advisor", logo: null, width: "170" },
|
||||
{ name: "Prosperty Platform", logo: null, width: "160" },
|
||||
{ name: "Moving Cargo", logo: null, width: "130" },
|
||||
{ name: "GSF Mobile", logo: null, width: "120" },
|
||||
{ name: "Farm Feeder", logo: null, width: "120" },
|
||||
{ name: "Melbourne City Card", logo: null, width: "170" },
|
||||
{ name: "ByteForge Labs", logo: null, width: "130" },
|
||||
{ name: "CodeCraft Studio", logo: null, width: "140" },
|
||||
{ name: "DevStream Tech", logo: null, width: "130" },
|
||||
{ name: "NextGen Solutions", logo: null, width: "150" },
|
||||
{ name: "ProdPush Platform", logo: null, width: "140" },
|
||||
{ name: "ScaleUp Ventures", logo: null, width: "140" },
|
||||
{ name: "AlphaVision Labs", logo: null, width: "140" },
|
||||
{ name: "CloudSync Systems", logo: null, width: "140" },
|
||||
{ name: "TechNova Group", logo: null, width: "130" },
|
||||
{ name: "DataFlow Pro", logo: null, width: "120" },
|
||||
{ name: "InnovateLab", logo: null, width: "120" }
|
||||
];
|
||||
|
||||
const countryFlags = [
|
||||
{
|
||||
name: "United States",
|
||||
alt: "United States flag icon",
|
||||
flagSvg: (
|
||||
<svg viewBox="0 0 24 18" className="w-8 h-6">
|
||||
<rect width="24" height="18" fill="#B22234"/>
|
||||
<rect width="24" height="1.38" y="1.38" fill="white"/>
|
||||
<rect width="24" height="1.38" y="4.15" fill="white"/>
|
||||
<rect width="24" height="1.38" y="6.92" fill="white"/>
|
||||
<rect width="24" height="1.38" y="9.69" fill="white"/>
|
||||
<rect width="24" height="1.38" y="12.46" fill="white"/>
|
||||
<rect width="24" height="1.38" y="15.23" fill="white"/>
|
||||
<rect width="9.6" height="9.69" fill="#3C3B6E"/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "United Kingdom",
|
||||
alt: "United Kingdom flag icon",
|
||||
flagSvg: (
|
||||
<svg viewBox="0 0 24 18" className="w-8 h-6">
|
||||
<rect width="24" height="18" fill="#012169"/>
|
||||
<path d="M0 0L24 18M24 0L0 18" stroke="white" strokeWidth="2"/>
|
||||
<path d="M0 0L24 18M24 0L0 18" stroke="#C8102E" strokeWidth="1"/>
|
||||
<path d="M12 0V18M0 9H24" stroke="white" strokeWidth="3"/>
|
||||
<path d="M12 0V18M0 9H24" stroke="#C8102E" strokeWidth="2"/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "India",
|
||||
alt: "India flag icon",
|
||||
flagSvg: (
|
||||
<svg viewBox="0 0 24 18" className="w-8 h-6">
|
||||
<rect width="24" height="6" fill="#FF9933"/>
|
||||
<rect width="24" height="6" y="6" fill="white"/>
|
||||
<rect width="24" height="6" y="12" fill="#138808"/>
|
||||
<circle cx="12" cy="9" r="2" fill="none" stroke="#000080" strokeWidth="0.3"/>
|
||||
<g transform="translate(12,9)">
|
||||
{Array.from({length: 24}, (_, i) => (
|
||||
<line key={i} x1="0" y1="-1.5" x2="0" y2="-1.8" stroke="#000080" strokeWidth="0.1" transform={`rotate(${i * 15})`}/>
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Canada",
|
||||
alt: "Canada flag icon",
|
||||
flagSvg: (
|
||||
<svg viewBox="0 0 24 18" className="w-8 h-6">
|
||||
<rect width="6" height="18" fill="#FF0000"/>
|
||||
<rect width="12" height="18" x="6" fill="white"/>
|
||||
<rect width="6" height="18" x="18" fill="#FF0000"/>
|
||||
<path d="M12 4L13 7H16L13.5 9L14.5 12L12 10L9.5 12L10.5 9L8 7H11L12 4Z" fill="#FF0000"/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "Australia",
|
||||
alt: "Australia flag icon",
|
||||
flagSvg: (
|
||||
<svg viewBox="0 0 24 18" className="w-8 h-6">
|
||||
<rect width="24" height="18" fill="#012169"/>
|
||||
<g transform="scale(0.5)">
|
||||
<rect width="24" height="9" fill="#012169"/>
|
||||
<path d="M0 0L24 18M24 0L0 18" stroke="white" strokeWidth="2"/>
|
||||
<path d="M0 0L24 18M24 0L0 18" stroke="#C8102E" strokeWidth="1"/>
|
||||
<path d="M12 0V18M0 9H24" stroke="white" strokeWidth="3"/>
|
||||
<path d="M12 0V18M0 9H24" stroke="#C8102E" strokeWidth="2"/>
|
||||
</g>
|
||||
<g fill="white">
|
||||
<circle cx="18" cy="6" r="0.5"/>
|
||||
<circle cx="20" cy="8" r="0.3"/>
|
||||
<circle cx="19" cy="10" r="0.4"/>
|
||||
<circle cx="21" cy="12" r="0.3"/>
|
||||
<circle cx="18" cy="14" r="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const ProjectImageCircles = () => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex justify-center items-center mb-12"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{countryFlags.map((flag, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: 0.5 + (index * 0.1),
|
||||
type: "spring",
|
||||
stiffness: 200
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
zIndex: 10,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
className="relative w-16 h-16 cursor-pointer group"
|
||||
style={{
|
||||
marginLeft: index > 0 ? '-8px' : '0',
|
||||
zIndex: countryFlags.length - index
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 w-16 h-16 rounded-full bg-white/10 backdrop-blur-sm border-2 border-white/20 group-hover:border-accent/50 group-hover:bg-white/15 transition-all duration-300 shadow-lg group-hover:shadow-xl flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className="w-10 h-8 flex items-center justify-center transform group-hover:scale-110 transition-transform duration-300"
|
||||
role="img"
|
||||
aria-label={flag.alt}
|
||||
>
|
||||
{flag.flagSvg}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle glow effect */}
|
||||
<div className="absolute inset-0 w-16 h-16 rounded-full bg-gradient-to-br from-accent/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-[#0E0E0E] text-white text-xs px-3 py-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none shadow-lg border border-white/10 z-50">
|
||||
<div className="text-center">
|
||||
<div className="font-medium text-accent">{flag.name}</div>
|
||||
</div>
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-3 border-r-3 border-t-3 border-transparent border-t-[#0E0E0E]"></div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const LogoCard = ({ name, width }: { name: string; width: string }) => (
|
||||
<div
|
||||
className="flex items-center justify-center h-16 bg-white/8 rounded-xl border border-white/10 hover:scale-105 hover:bg-white/12 hover:border-accent/20 transition-all duration-300 px-6 shadow-lg backdrop-blur-sm group"
|
||||
style={{ minWidth: `${width}px` }}
|
||||
>
|
||||
<span className="text-white/80 font-medium text-sm text-center leading-tight group-hover:text-white/95 transition-colors duration-300">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MarqueeRow = ({ logos }: { logos: typeof companyLogos }) => (
|
||||
<motion.div
|
||||
animate={{
|
||||
x: [0, -3200],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 120,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
className="flex gap-6 items-center"
|
||||
style={{
|
||||
width: "fit-content",
|
||||
}}
|
||||
>
|
||||
{[...logos, ...logos].map((company, index) => (
|
||||
<LogoCard key={`${company.name}-${index}`} name={company.name} width={company.width} />
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export const ClientLogos = () => {
|
||||
return (
|
||||
<section className="relative py-20 bg-[#121212] border-y border-white/5 overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-8"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-6">
|
||||
Trusted by Founders and CTOs Across 15+ Countries
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Project Image Circles */}
|
||||
<ProjectImageCircles />
|
||||
|
||||
{/* Company Logos Ticker */}
|
||||
<div className="overflow-hidden">
|
||||
<MarqueeRow logos={companyLogos} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
205
components/CookieConsent.tsx
Normal file
205
components/CookieConsent.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Cookie, Shield, Settings } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export const CookieConsent = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already made a choice
|
||||
const consent = localStorage.getItem('cookieConsent');
|
||||
if (!consent) {
|
||||
// Show banner after a short delay
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
localStorage.setItem('cookieConsent', 'accepted');
|
||||
localStorage.setItem('cookiePreferences', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
functional: true
|
||||
}));
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleDeclineAll = () => {
|
||||
localStorage.setItem('cookieConsent', 'declined');
|
||||
localStorage.setItem('cookiePreferences', JSON.stringify({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
functional: false
|
||||
}));
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleSavePreferences = () => {
|
||||
localStorage.setItem('cookieConsent', 'customized');
|
||||
// In a real app, you would save the specific preferences here
|
||||
setIsVisible(false);
|
||||
setShowSettings(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="fixed bottom-0 left-0 right-0 z-50 bg-black border-t border-white/10 shadow-2xl"
|
||||
>
|
||||
<div className="container mx-auto px-4 lg:px-8">
|
||||
{!showSettings ? (
|
||||
// Main Cookie Consent Banner
|
||||
<div className="py-4 lg:py-6">
|
||||
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-4 lg:gap-6">
|
||||
{/* Icon and Message */}
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="w-8 h-8 bg-[#E5195E]/20 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Cookie className="w-4 h-4 text-[#E5195E]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-white font-semibold text-sm mb-1">
|
||||
We use cookies to enhance your experience
|
||||
</h3>
|
||||
<p className="text-[#CCCCCC] text-sm leading-relaxed">
|
||||
We use cookies to analyze site performance, deliver personalized content, and improve your browsing experience.
|
||||
By clicking "Accept All", you consent to our use of cookies.{' '}
|
||||
<a href="/privacy" className="text-[#E5195E] hover:text-[#E5195E]/80 underline">
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full lg:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-white/20 text-white hover:bg-white/10 hover:border-[#E5195E]/50 hover:text-white transition-all duration-300"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Customize
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-white/20 text-white hover:bg-white/10 hover:border-white/30 hover:text-white transition-all duration-300"
|
||||
onClick={handleDeclineAll}
|
||||
>
|
||||
Decline All
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white transition-all duration-300"
|
||||
onClick={handleAcceptAll}
|
||||
>
|
||||
Accept All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute top-4 right-4 lg:relative lg:top-auto lg:right-auto w-8 h-8 flex items-center justify-center text-[#CCCCCC] hover:text-white hover:bg-white/10 rounded-lg transition-all duration-300"
|
||||
aria-label="Close cookie banner"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Cookie Settings Panel
|
||||
<div className="py-6">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-8 h-8 bg-[#E5195E]/20 rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-[#E5195E]" />
|
||||
</div>
|
||||
<h3 className="text-white font-semibold text-lg">Cookie Preferences</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
{/* Necessary Cookies */}
|
||||
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Necessary Cookies</h4>
|
||||
<div className="w-12 h-6 bg-[#E5195E] rounded-full flex items-center justify-end px-1">
|
||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#CCCCCC] text-sm">
|
||||
These cookies are essential for the website to function properly. They cannot be disabled.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Analytics Cookies */}
|
||||
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Analytics Cookies</h4>
|
||||
<div className="w-12 h-6 bg-white/20 rounded-full flex items-center justify-start px-1">
|
||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#CCCCCC] text-sm">
|
||||
Help us understand how visitors interact with our website by collecting anonymous information.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Marketing Cookies */}
|
||||
<div className="bg-white/5 rounded-lg p-4 border border-white/10">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-white font-medium">Marketing Cookies</h4>
|
||||
<div className="w-12 h-6 bg-white/20 rounded-full flex items-center justify-start px-1">
|
||||
<div className="w-4 h-4 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#CCCCCC] text-sm">
|
||||
Used to track visitors across websites to display relevant advertisements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-white/20 text-white hover:bg-white/10 hover:border-white/30 hover:text-white transition-all duration-300"
|
||||
onClick={() => setShowSettings(false)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white transition-all duration-300"
|
||||
onClick={handleSavePreferences}
|
||||
>
|
||||
Save Preferences
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
192
components/CountryFlags.tsx
Normal file
192
components/CountryFlags.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const foundersAndCTOs = [
|
||||
{
|
||||
name: "SimpliTend",
|
||||
title: "HealthTech Platform",
|
||||
country: "India",
|
||||
code: "IN",
|
||||
flagEmoji: "🇮🇳",
|
||||
projectType: "Care Management"
|
||||
},
|
||||
{
|
||||
name: "Seezun",
|
||||
title: "Fashion Marketplace",
|
||||
country: "United Kingdom",
|
||||
code: "GB",
|
||||
flagEmoji: "🇬🇧",
|
||||
projectType: "P2P Platform"
|
||||
},
|
||||
{
|
||||
name: "AMOZ",
|
||||
title: "E-commerce Platform",
|
||||
country: "United States",
|
||||
code: "US",
|
||||
flagEmoji: "🇺🇸",
|
||||
projectType: "Digital Commerce"
|
||||
},
|
||||
{
|
||||
name: "TradersCircuit",
|
||||
title: "Trading Platform",
|
||||
country: "United Arab Emirates",
|
||||
code: "AE",
|
||||
flagEmoji: "🇦🇪",
|
||||
projectType: "FinTech"
|
||||
},
|
||||
{
|
||||
name: "FreeU",
|
||||
title: "Social Platform",
|
||||
country: "Australia",
|
||||
code: "AU",
|
||||
flagEmoji: "🇦🇺",
|
||||
projectType: "Community"
|
||||
},
|
||||
{
|
||||
name: "Dorf Ketal",
|
||||
title: "Manufacturing Tech",
|
||||
country: "Germany",
|
||||
code: "DE",
|
||||
flagEmoji: "🇩🇪",
|
||||
projectType: "Industrial IoT"
|
||||
},
|
||||
{
|
||||
name: "WOKA",
|
||||
title: "Learning Platform",
|
||||
country: "Singapore",
|
||||
code: "SG",
|
||||
flagEmoji: "🇸🇬",
|
||||
projectType: "EdTech"
|
||||
},
|
||||
{
|
||||
name: "Regroup",
|
||||
title: "Sports Networking",
|
||||
country: "Canada",
|
||||
code: "CA",
|
||||
flagEmoji: "🇨🇦",
|
||||
projectType: "Social Sports"
|
||||
},
|
||||
{
|
||||
name: "Tanami Capital",
|
||||
title: "Wealth Management",
|
||||
country: "Brazil",
|
||||
code: "BR",
|
||||
flagEmoji: "🇧🇷",
|
||||
projectType: "FinTech"
|
||||
},
|
||||
{
|
||||
name: "SSA",
|
||||
title: "Networking Platform",
|
||||
country: "Japan",
|
||||
code: "JP",
|
||||
flagEmoji: "🇯🇵",
|
||||
projectType: "Professional Network"
|
||||
},
|
||||
{
|
||||
name: "Amble",
|
||||
title: "Travel Platform",
|
||||
country: "France",
|
||||
code: "FR",
|
||||
flagEmoji: "🇫🇷",
|
||||
projectType: "Travel Tech"
|
||||
},
|
||||
{
|
||||
name: "Agromate",
|
||||
title: "AgriTech Solution",
|
||||
country: "Netherlands",
|
||||
code: "NL",
|
||||
flagEmoji: "🇳🇱",
|
||||
projectType: "Agriculture"
|
||||
},
|
||||
{
|
||||
name: "Moving Cargo",
|
||||
title: "Logistics Platform",
|
||||
country: "Sweden",
|
||||
code: "SE",
|
||||
flagEmoji: "🇸🇪",
|
||||
projectType: "Supply Chain"
|
||||
},
|
||||
{
|
||||
name: "Farm Feeder",
|
||||
title: "Agricultural Tech",
|
||||
country: "New Zealand",
|
||||
code: "NZ",
|
||||
flagEmoji: "🇳🇿",
|
||||
projectType: "AgriTech"
|
||||
},
|
||||
{
|
||||
name: "Melbourne City Card",
|
||||
title: "City Services Platform",
|
||||
country: "South Korea",
|
||||
code: "KR",
|
||||
flagEmoji: "🇰🇷",
|
||||
projectType: "Civic Tech"
|
||||
}
|
||||
];
|
||||
|
||||
export const CountryFlags = () => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="mb-12"
|
||||
>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6 max-w-7xl mx-auto">
|
||||
{foundersAndCTOs.map((project, index) => (
|
||||
<motion.div
|
||||
key={project.code + index}
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.5,
|
||||
delay: 0.5 + (index * 0.1),
|
||||
type: "spring",
|
||||
stiffness: 200
|
||||
}}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
className="group cursor-pointer relative"
|
||||
>
|
||||
<div className="text-center">
|
||||
{/* Flag Icon */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<div className="w-14 h-14 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 flex items-center justify-center group-hover:bg-white/20 group-hover:border-accent/30 transition-all duration-300 shadow-lg group-hover:shadow-xl">
|
||||
<span className="text-2xl" role="img" aria-label={`${project.country} flag`}>
|
||||
{project.flagEmoji}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Badge */}
|
||||
<div className="bg-white/8 rounded-lg border border-white/10 px-3 py-2.5 group-hover:bg-white/12 group-hover:border-accent/30 transition-all duration-300 shadow-sm backdrop-blur-sm min-h-[60px] flex flex-col justify-center">
|
||||
<div className="text-white/90 font-medium text-sm leading-tight mb-1 group-hover:text-white transition-colors duration-300">
|
||||
{project.name}
|
||||
</div>
|
||||
<div className="text-white/60 text-xs leading-tight">
|
||||
{project.projectType}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Tooltip */}
|
||||
<div className="absolute -top-24 left-1/2 transform -translate-x-1/2 bg-[#0E0E0E] text-white text-xs px-4 py-3 rounded-xl opacity-0 group-hover:opacity-100 transition-all duration-300 whitespace-nowrap pointer-events-none shadow-xl border border-white/20 z-50 backdrop-blur-md">
|
||||
<div className="text-center">
|
||||
<div className="font-semibold text-accent mb-1">{project.name}</div>
|
||||
<div className="text-white/80 mb-1">{project.title}</div>
|
||||
<div className="text-white/60 flex items-center gap-1 justify-center">
|
||||
<span>{project.flagEmoji}</span>
|
||||
<span>{project.country}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[#0E0E0E]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
160
components/CustomReCaptcha.tsx
Normal file
160
components/CustomReCaptcha.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
interface CustomReCaptchaProps {
|
||||
siteKey: string;
|
||||
onVerify: (token: string) => void;
|
||||
onExpired?: () => void;
|
||||
onError?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ReCaptchaRef {
|
||||
reset: () => void;
|
||||
execute: () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
grecaptcha: any;
|
||||
}
|
||||
}
|
||||
|
||||
const CustomReCaptcha = forwardRef<ReCaptchaRef, CustomReCaptchaProps>(({
|
||||
siteKey,
|
||||
onVerify,
|
||||
onExpired,
|
||||
onError,
|
||||
className = ""
|
||||
}, ref) => {
|
||||
const captchaRef = useRef<HTMLDivElement>(null);
|
||||
const widgetId = useRef<number | null>(null);
|
||||
const isLoadedRef = useRef(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
if (window.grecaptcha && widgetId.current !== null) {
|
||||
window.grecaptcha.reset(widgetId.current);
|
||||
}
|
||||
},
|
||||
execute: () => {
|
||||
if (window.grecaptcha && widgetId.current !== null) {
|
||||
window.grecaptcha.execute(widgetId.current);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const loadReCaptcha = () => {
|
||||
if (window.grecaptcha) {
|
||||
renderReCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load reCAPTCHA script
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://www.google.com/recaptcha/api.js?onload=onReCaptchaLoad&render=explicit';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
// Set up callback for when script loads
|
||||
(window as any).onReCaptchaLoad = () => {
|
||||
renderReCaptcha();
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
|
||||
const renderReCaptcha = () => {
|
||||
if (!captchaRef.current || isLoadedRef.current) return;
|
||||
|
||||
try {
|
||||
widgetId.current = window.grecaptcha.render(captchaRef.current, {
|
||||
sitekey: siteKey,
|
||||
callback: onVerify,
|
||||
'expired-callback': onExpired,
|
||||
'error-callback': onError,
|
||||
theme: 'dark',
|
||||
size: 'normal'
|
||||
});
|
||||
isLoadedRef.current = true;
|
||||
} catch (error) {
|
||||
console.error('Error rendering reCAPTCHA:', error);
|
||||
if (onError) onError();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadReCaptcha();
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
if (window.grecaptcha && widgetId.current !== null) {
|
||||
try {
|
||||
window.grecaptcha.reset(widgetId.current);
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Add styles to document head instead of using styled-jsx
|
||||
useEffect(() => {
|
||||
const styleId = 'custom-recaptcha-styles';
|
||||
|
||||
// Check if styles are already added
|
||||
if (document.getElementById(styleId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.id = styleId;
|
||||
styleElement.textContent = `
|
||||
.grecaptcha-badge {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
iframe[src*="recaptcha"] {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-recaptcha {
|
||||
transform: scale(1);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.g-recaptcha > div {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
// Cleanup function to remove styles when component unmounts
|
||||
return () => {
|
||||
const existingStyle = document.getElementById(styleId);
|
||||
if (existingStyle) {
|
||||
document.head.removeChild(existingStyle);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`flex justify-center ${className}`}>
|
||||
<div
|
||||
className="bg-gray-800/30 border border-gray-600/50 rounded-xl p-6 shadow-lg backdrop-blur-sm"
|
||||
style={{
|
||||
'--recaptcha-border-radius': '12px'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<div ref={captchaRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CustomReCaptcha.displayName = 'CustomReCaptcha';
|
||||
|
||||
export default CustomReCaptcha;
|
||||
79
components/FAQSection.tsx
Normal file
79
components/FAQSection.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
interface FAQ {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSectionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
faqs: FAQ[];
|
||||
}
|
||||
|
||||
export const FAQSection: React.FC<FAQSectionProps> = ({
|
||||
title = "Frequently Asked Questions",
|
||||
subtitle,
|
||||
faqs
|
||||
}) => {
|
||||
return (
|
||||
<section className="py-20 bg-black">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-lg text-gray-400 max-w-2xl mx-auto">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
<Accordion type="single" collapsible className="w-full space-y-4">
|
||||
{faqs.map((faq, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<AccordionItem
|
||||
value={`item-${index}`}
|
||||
className="border-none bg-slate-800/40 rounded-xl overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger
|
||||
className="text-left text-white hover:text-white hover:no-underline px-6 py-6 text-lg font-medium [&[data-state=open]>svg]:rotate-180"
|
||||
>
|
||||
<span className="flex-1 text-left">{faq.question}</span>
|
||||
<ChevronDown className="h-5 w-5 shrink-0 text-white transition-transform duration-200" />
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-gray-300 px-6 pb-6 pt-0 text-base leading-relaxed">
|
||||
{faq.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</motion.div>
|
||||
))}
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
497
components/FeaturedCaseStudies.tsx
Normal file
497
components/FeaturedCaseStudies.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
import { navigateTo } from "../App";
|
||||
import {
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
Eye,
|
||||
ShoppingCart,
|
||||
Heart,
|
||||
Star,
|
||||
Clock,
|
||||
Target,
|
||||
Smartphone,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Network,
|
||||
Search,
|
||||
Calendar,
|
||||
PlayCircle,
|
||||
PartyPopper,
|
||||
PieChart
|
||||
} from "lucide-react";
|
||||
|
||||
const FeaturedCaseStudies = () => {
|
||||
const caseStudies = [
|
||||
{
|
||||
id: 1,
|
||||
title: "SimplyTend",
|
||||
client: "Simply Tend",
|
||||
description: "SimpliTend is a mobile-first care management platform that connects patients and caregivers through real-time alerts, scheduling, and safety tools—delivered via dual apps and an admin dashboard.",
|
||||
keyAchievement: {
|
||||
number: "95%",
|
||||
metric: "Care Coordination Efficiency",
|
||||
icon: Heart
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Mobile App", "Care Management", "Real-Time Alerts", "Scheduling"],
|
||||
gradient: "from-blue-500/20 to-cyan-500/20",
|
||||
accentColor: "blue",
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Seezun",
|
||||
client: "Seezun",
|
||||
description: "Seezun is a trend-driven P2P fashion marketplace enabling users to rent, buy, sell, or lend South Asian ethnicwear via mobile and web platforms.",
|
||||
keyAchievement: {
|
||||
number: "85%",
|
||||
metric: "Brand Recognition",
|
||||
icon: TrendingUp
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Marketplace", "P2P", "Fashion", "Mobile & Web"],
|
||||
gradient: "from-purple-500/20 to-pink-500/20",
|
||||
accentColor: "purple",
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "WOKA",
|
||||
client: "WOKA Creations Pvt. Ltd",
|
||||
description: "WDI transformed WOKA's hybrid app into a high-performance native Android and iOS platform featuring seamless streaming, deep analytics, and robust 120-hour monthly support.",
|
||||
keyAchievement: {
|
||||
number: "300%",
|
||||
metric: "User Retention",
|
||||
icon: Users
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1503676260728-1c00da094a0b?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Native App", "Streaming", "Analytics", "Support"],
|
||||
gradient: "from-green-500/20 to-emerald-500/20",
|
||||
accentColor: "green",
|
||||
featured: false
|
||||
}
|
||||
];
|
||||
|
||||
const moreSuccessStories = [
|
||||
{
|
||||
id: 4,
|
||||
title: "TradersCircuit",
|
||||
client: "TradersCircuit",
|
||||
description: "TradersCircuit empowers India's millennials and Gen Z with smarter investments through seamless investment experience and ultra-personalized financial planning.",
|
||||
keyAchievement: {
|
||||
number: "300%",
|
||||
metric: "User Growth",
|
||||
icon: TrendingUp
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["FinTech", "Trading Platform", "Indian Market", "Mobile App"],
|
||||
gradient: "from-green-500/20 to-emerald-500/20",
|
||||
accentColor: "green",
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "RanOutOf",
|
||||
client: "Global Ease Solutions",
|
||||
description: "WDI developed a smart grocery planning app with barcode scanning, voice commands, reminders, and a web-based admin CMS for Global Ease Solutions.",
|
||||
keyAchievement: {
|
||||
number: "75%",
|
||||
metric: "Shopping Efficiency",
|
||||
icon: ShoppingCart
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1542838132-92c53300491e?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Mobile App", "Barcode Scanning", "Voice AI", "Grocery Tech"],
|
||||
gradient: "from-green-500/20 to-emerald-500/20",
|
||||
accentColor: "green",
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Prosperty",
|
||||
client: "Prosperty Ltd",
|
||||
description: "Break the barrier of real estate investing. With Prosperty, you can invest in portions of properties, making portfolio diversification smarter and more accessible.",
|
||||
keyAchievement: {
|
||||
number: "300%",
|
||||
metric: "Portfolio Options",
|
||||
icon: PieChart
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Real Estate", "Investment", "FinTech", "Portfolio"],
|
||||
gradient: "from-blue-500/20 to-cyan-500/20",
|
||||
accentColor: "blue",
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "GoodTimes",
|
||||
client: "GoodTimes Ltd",
|
||||
description: "From casual hangouts to special celebrations, Good Times makes browsing and booking a breeze, so you never miss out.",
|
||||
keyAchievement: {
|
||||
number: "250%",
|
||||
metric: "Event Discovery",
|
||||
icon: PartyPopper
|
||||
},
|
||||
visual: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=600&h=400&fit=crop&auto=format",
|
||||
tags: ["Events", "Booking", "Lifestyle", "Mobile App"],
|
||||
gradient: "from-purple-500/20 to-pink-500/20",
|
||||
accentColor: "purple",
|
||||
featured: false
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-black">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Section Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="mb-6"
|
||||
>
|
||||
<div className="inline-block p-[2px] rounded-full bg-gradient-to-r from-accent via-blue-500 to-purple-500">
|
||||
<div className="bg-black rounded-full px-6 py-3 flex items-center gap-2">
|
||||
<Star className="w-5 h-5 text-accent" />
|
||||
<span className="text-white text-base font-medium">Featured Work</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-white mb-6">
|
||||
Featured Success Stories
|
||||
</h2>
|
||||
<p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||
Discover how we've helped companies across industries achieve remarkable results with our innovative development solutions.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Case Studies Grid - Consistent Dimensions */}
|
||||
<div className="grid lg:grid-cols-3 gap-8 items-stretch">
|
||||
{caseStudies.map((study, index) => {
|
||||
const AchievementIcon = study.keyAchievement.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={study.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="group h-full"
|
||||
>
|
||||
<Card
|
||||
className="bg-gray-900/50 backdrop-blur-md border-gray-800 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl rounded-2xl overflow-hidden h-full group-hover:scale-[1.02] transform flex flex-col cursor-pointer"
|
||||
onClick={() => {
|
||||
if (study.title === 'Seezun') {
|
||||
navigateTo('/projects/seezun');
|
||||
} else if (study.title === 'WOKA') {
|
||||
navigateTo('/projects/woka');
|
||||
} else if (study.title === 'Tanami') {
|
||||
navigateTo('/projects/tanami');
|
||||
} else {
|
||||
navigateTo('/case-studies');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-0 flex flex-col h-full min-h-[600px]">
|
||||
{/* Visual Section - Fixed Height */}
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="relative h-64 w-full">
|
||||
<ImageWithFallback
|
||||
src={study.visual}
|
||||
alt={study.title}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Overlay with gradient */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-t ${study.gradient} opacity-20 group-hover:opacity-40 transition-opacity duration-500`} />
|
||||
|
||||
{/* Featured Badge */}
|
||||
{study.featured && (
|
||||
<div className="absolute top-4 left-4">
|
||||
<Badge className="bg-accent text-white shadow-lg">
|
||||
<Star className="w-3 h-3 mr-1" />
|
||||
Featured
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Key Achievement - Overlaid on Visual */}
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="bg-black/80 backdrop-blur-md rounded-xl p-4 border border-white/10"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-gradient-to-r ${
|
||||
study.accentColor === 'blue' ? 'from-blue-500 to-cyan-500' :
|
||||
study.accentColor === 'green' ? 'from-green-500 to-emerald-500' :
|
||||
study.accentColor === 'purple' ? 'from-purple-500 to-pink-500' :
|
||||
study.accentColor === 'cyan' ? 'from-cyan-500 to-blue-500' :
|
||||
study.accentColor === 'orange' ? 'from-orange-500 to-red-500' :
|
||||
'from-emerald-500 to-teal-500'
|
||||
} flex items-center justify-center flex-shrink-0`}>
|
||||
<AchievementIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-2xl font-bold text-white">{study.keyAchievement.number}</div>
|
||||
<div className="text-sm text-gray-300 leading-tight">{study.keyAchievement.metric}</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section - Flexible Height with Consistent Spacing */}
|
||||
<div className="p-6 flex-1 flex flex-col justify-between min-h-[336px]">
|
||||
<div className="flex-1">
|
||||
{/* Project Title - Consistent Height */}
|
||||
<div className="mb-4 min-h-[60px] flex items-start">
|
||||
<h3 className="text-xl font-semibold text-white leading-tight group-hover:text-accent transition-colors duration-300 line-clamp-2">
|
||||
{study.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Client & Description - Consistent Height */}
|
||||
<div className="mb-6 min-h-[100px]">
|
||||
<div className="text-accent font-medium text-sm mb-2">{study.client}</div>
|
||||
<p className="text-gray-300 text-sm leading-relaxed line-clamp-4">
|
||||
{study.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tags - Consistent Height */}
|
||||
<div className="mb-6 min-h-[32px]">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{study.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="text-xs bg-gray-800/50 text-gray-300 border-gray-700 hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button - Fixed at Bottom */}
|
||||
<div className="mt-auto">
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70 text-white font-semibold py-3 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group h-12"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (study.title === 'Seezun') {
|
||||
navigateTo('/projects/seezun');
|
||||
} else if (study.title === 'WOKA') {
|
||||
navigateTo('/projects/woka');
|
||||
} else if (study.title === 'Tanami') {
|
||||
navigateTo('/projects/tanami');
|
||||
} else {
|
||||
navigateTo('/case-studies');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>View Full Case Study</span>
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* More Success Stories Section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="mt-20 mb-16"
|
||||
>
|
||||
<h3 className="text-3xl lg:text-4xl font-semibold text-white mb-12 text-center">
|
||||
More Success Stories
|
||||
</h3>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-8 items-stretch">
|
||||
{moreSuccessStories.map((story, index) => {
|
||||
const AchievementIcon = story.keyAchievement.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={story.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
className="group h-full"
|
||||
>
|
||||
<Card
|
||||
className="bg-gray-900/50 backdrop-blur-md border-gray-800 hover:border-accent/30 transition-all duration-500 shadow-lg hover:shadow-2xl rounded-2xl overflow-hidden h-full group-hover:scale-[1.02] transform flex flex-col cursor-pointer"
|
||||
onClick={() => {
|
||||
if (story.title === 'TradersCircuit') {
|
||||
navigateTo('/projects/traderscircuit');
|
||||
} else if (story.title === 'GoodTimes') {
|
||||
navigateTo('/projects/goodtimes');
|
||||
} else if (story.title === 'Prosperty') {
|
||||
navigateTo('/projects/prosperty');
|
||||
} else if (story.title === 'RanOutOf') {
|
||||
navigateTo('/projects/ranoutof');
|
||||
} else {
|
||||
navigateTo('/case-studies');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-0 flex flex-col h-full min-h-[600px]">
|
||||
{/* Visual Section - Fixed Height */}
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="relative h-64 w-full">
|
||||
<ImageWithFallback
|
||||
src={story.visual}
|
||||
alt={`${story.title} - ${story.client}`}
|
||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||
/>
|
||||
|
||||
{/* Overlay with gradient */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-t ${story.gradient} opacity-20 group-hover:opacity-40 transition-opacity duration-500`} />
|
||||
|
||||
{/* Key Achievement - Overlaid on Visual */}
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="bg-black/80 backdrop-blur-md rounded-xl p-4 border border-white/10"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg bg-gradient-to-r ${
|
||||
story.accentColor === 'blue' ? 'from-blue-500 to-cyan-500' :
|
||||
story.accentColor === 'green' ? 'from-green-500 to-emerald-500' :
|
||||
story.accentColor === 'purple' ? 'from-purple-500 to-pink-500' :
|
||||
story.accentColor === 'cyan' ? 'from-cyan-500 to-blue-500' :
|
||||
story.accentColor === 'orange' ? 'from-orange-500 to-red-500' :
|
||||
'from-emerald-500 to-teal-500'
|
||||
} flex items-center justify-center flex-shrink-0`}>
|
||||
<AchievementIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-2xl font-bold text-white">{story.keyAchievement.number}</div>
|
||||
<div className="text-sm text-gray-300 leading-tight">{story.keyAchievement.metric}</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section - Flexible Height with Consistent Spacing */}
|
||||
<div className="p-6 flex-1 flex flex-col justify-between min-h-[336px]">
|
||||
<div className="flex-1">
|
||||
{/* Project Title - Consistent Height */}
|
||||
<div className="mb-4 min-h-[60px] flex items-start">
|
||||
<h4 className="text-xl font-semibold text-white leading-tight group-hover:text-accent transition-colors duration-300 line-clamp-2">
|
||||
{story.title}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* Client & Description - Consistent Height */}
|
||||
<div className="mb-6 min-h-[100px]">
|
||||
<div className="text-accent font-medium text-sm mb-2">{story.client}</div>
|
||||
<p className="text-gray-300 text-sm leading-relaxed line-clamp-4">
|
||||
{story.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tags - Consistent Height */}
|
||||
<div className="mb-6 min-h-[32px]">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{story.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="text-xs bg-gray-800/50 text-gray-300 border-gray-700 hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Button - Fixed at Bottom */}
|
||||
<div className="mt-auto">
|
||||
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-accent to-accent/80 hover:from-accent/90 hover:to-accent/70 text-white font-semibold py-3 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 group h-12"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (story.title === 'TradersCircuit') {
|
||||
navigateTo('/projects/traderscircuit');
|
||||
} else if (story.title === 'GoodTimes') {
|
||||
navigateTo('/projects/goodtimes');
|
||||
} else if (story.title === 'Prosperty') {
|
||||
navigateTo('/projects/prosperty');
|
||||
} else if (story.title === 'RanOutOf') {
|
||||
navigateTo('/projects/ranoutof');
|
||||
} else {
|
||||
navigateTo('/case-studies');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>View Full Case Study</span>
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-16"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="border-gray-600 text-gray-300 hover:bg-gray-800 hover:text-white hover:border-accent/50 transition-all duration-300"
|
||||
>
|
||||
<Eye className="w-5 h-5 mr-2" />
|
||||
View All Case Studies
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturedCaseStudies;
|
||||
426
components/Footer.tsx
Normal file
426
components/Footer.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
Linkedin,
|
||||
Twitter,
|
||||
Github,
|
||||
Youtube,
|
||||
} from "lucide-react";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import BlackLogo14 from "../imports/BlackLogo14";
|
||||
import { navigateTo } from "../App";
|
||||
import { useState } from "react";
|
||||
|
||||
const footerNavigation = {
|
||||
Explore: [
|
||||
{ label: "Home", url: "/home" },
|
||||
{ label: "Services", url: "/services" },
|
||||
{ label: "Solutions", url: "/solutions" },
|
||||
{ label: "Industries", url: "/industries" },
|
||||
{ label: "Company", url: "/company" },
|
||||
{ label: "Contact", url: "/contact" },
|
||||
],
|
||||
Resources: [
|
||||
{ label: "Articles", url: "/resources/blog" },
|
||||
{ label: "Case Studies", url: "/case-studies" },
|
||||
{
|
||||
label: "Client Testimonials",
|
||||
url: "/resources/client-testimonials",
|
||||
},
|
||||
{
|
||||
label: "Whitepapers & Insights",
|
||||
url: "/resources/whitepapers-insights",
|
||||
},
|
||||
{ label: "FAQ", url: "/resources/faqs" },
|
||||
],
|
||||
Services: [
|
||||
{
|
||||
label: "Mobile App Development",
|
||||
url: "/services/mobile-app-development",
|
||||
},
|
||||
{
|
||||
label: "Web & Cloud Solutions",
|
||||
url: "/services/web-cloud-solutions",
|
||||
},
|
||||
{
|
||||
label: "Software Engineering",
|
||||
url: "/services/software-engineering",
|
||||
},
|
||||
{
|
||||
label: "Design & Experience",
|
||||
url: "/services/design-experience",
|
||||
},
|
||||
],
|
||||
"AI & ML": [
|
||||
{
|
||||
label: "Artificial Intelligence Services",
|
||||
url: "/ai/artificial-intelligence-services",
|
||||
},
|
||||
{
|
||||
label: "Machine Learning Solutions",
|
||||
url: "/ai/machine-learning-solutions",
|
||||
},
|
||||
],
|
||||
Solutions: [
|
||||
{
|
||||
label: "Digital Product Development",
|
||||
url: "/digital-product-development",
|
||||
},
|
||||
{
|
||||
label: "MVP & Startup Launch Packages",
|
||||
url: "/mvp-startup-launch",
|
||||
},
|
||||
{
|
||||
label: "Legacy System Rebuilds",
|
||||
url: "/legacy-system-rebuilds",
|
||||
},
|
||||
{
|
||||
label: "Dedicated Development Centers",
|
||||
url: "/dedicated-development-centers",
|
||||
},
|
||||
{
|
||||
label: "Business Process Automation",
|
||||
url: "/business-process-automation",
|
||||
},
|
||||
{
|
||||
label: "Compliance-Ready Systems",
|
||||
url: "/compliance-ready-systems",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const socialLinks = [
|
||||
{
|
||||
name: "LinkedIn",
|
||||
icon: Linkedin,
|
||||
url: "https://linkedin.com/company/wdi",
|
||||
},
|
||||
{
|
||||
name: "Twitter",
|
||||
icon: Twitter,
|
||||
url: "https://twitter.com/wdi_dev",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
icon: Github,
|
||||
url: "https://github.com/wdi",
|
||||
},
|
||||
{
|
||||
name: "YouTube",
|
||||
icon: Youtube,
|
||||
url: "https://youtube.com/wdi",
|
||||
},
|
||||
];
|
||||
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: Mail,
|
||||
label: "ideas@wdipl.com",
|
||||
url: "mailto:ideas@wdipl.com",
|
||||
},
|
||||
{
|
||||
icon: Phone,
|
||||
label: "(+91) 7700900039",
|
||||
url: "tel:+917700900039",
|
||||
},
|
||||
{
|
||||
icon: MapPin,
|
||||
label:
|
||||
"614, Palm Spring Centre, Link Road, Malad (West), Mumbai - 400064. India.",
|
||||
url: "#",
|
||||
},
|
||||
];
|
||||
|
||||
const FooterSection = ({
|
||||
title,
|
||||
links,
|
||||
delay = 0,
|
||||
}: {
|
||||
title: string;
|
||||
links: Array<{ label: string; url: string }>;
|
||||
delay?: number;
|
||||
}) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay }}
|
||||
viewport={{ once: true }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<h4 className="font-semibold text-white text-lg">
|
||||
{title}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{links.map((link) => (
|
||||
<li key={link.label}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigateTo(link.url);
|
||||
}}
|
||||
className="text-[#CCCCCC] hover:text-white transition-colors duration-200 text-sm block py-1 hover:translate-x-1 transform cursor-pointer"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
// Newsletter subscription component
|
||||
const NewsletterSection = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubscribe = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
setIsSubscribed(true);
|
||||
setIsSubmitting(false);
|
||||
setEmail("");
|
||||
|
||||
// Reset success message after 3 seconds
|
||||
setTimeout(() => setIsSubscribed(false), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="border-t border-white/10"
|
||||
>
|
||||
<div className="container mx-auto px-6 lg:px-8 py-16">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h3 className="text-2xl lg:text-3xl font-semibold text-white mb-4">
|
||||
Never Miss an Update
|
||||
</h3>
|
||||
<p className="text-[#CCCCCC] text-lg mb-8 max-w-2xl mx-auto">
|
||||
Get the latest insights on digital product
|
||||
development, AI trends, and startup success stories
|
||||
delivered to your inbox.
|
||||
</p>
|
||||
|
||||
{isSubscribed ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-green-500/10 border border-green-500/20 rounded-lg p-6 max-w-md mx-auto"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2 text-green-400">
|
||||
<Mail className="w-5 h-5" />
|
||||
<span className="font-medium">
|
||||
Successfully subscribed!
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-green-300 text-sm mt-2">
|
||||
Welcome to our community of innovators.
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubscribe}
|
||||
className="max-w-md mx-auto"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Enter your email address"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="flex-1 bg-white/5 border-white/10 text-white placeholder:text-[#CCCCCC] focus:border-[#E5195E] focus:ring-[#E5195E]/20"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-[#E5195E] hover:bg-[#E5195E]/90 text-white px-6 shrink-0 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting
|
||||
? "Subscribing..."
|
||||
: "Subscribe"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[#CCCCCC] text-xs mt-3">
|
||||
No spam, unsubscribe at any time. We respect
|
||||
your privacy.
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer className="relative bg-[#0E0E0E] border-t border-white/10 overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Main Footer Content */}
|
||||
<div className="container mx-auto px-6 lg:px-8 py-16">
|
||||
<div className="grid lg:grid-cols-7 gap-12">
|
||||
{/* Company Info */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="lg:col-span-2 space-y-6"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12">
|
||||
<BlackLogo14 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[#CCCCCC] leading-relaxed max-w-md">
|
||||
Web Development Institute - Transforming ideas
|
||||
into scalable digital products. 25+ years of
|
||||
industry expertise, serving founders and CTOs
|
||||
across 15+ countries.
|
||||
</p>
|
||||
|
||||
{/* India Office Contact Information */}
|
||||
<div className="space-y-4">
|
||||
<h5 className="font-semibold text-white text-sm tracking-wide uppercase">
|
||||
India Office
|
||||
</h5>
|
||||
<div className="space-y-3">
|
||||
{contactInfo.map((contact) => {
|
||||
const Icon = contact.icon;
|
||||
return (
|
||||
<a
|
||||
key={contact.label}
|
||||
href={contact.url}
|
||||
className="flex items-start gap-3 text-[#CCCCCC] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-[#E5195E] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm leading-relaxed">
|
||||
{contact.label}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex gap-4 pt-4">
|
||||
{socialLinks.map((social) => {
|
||||
const Icon = social.icon;
|
||||
return (
|
||||
<a
|
||||
key={social.name}
|
||||
href={social.url}
|
||||
className="w-10 h-10 bg-white/5 rounded-lg flex items-center justify-center text-[#CCCCCC] hover:text-white hover:bg-[#E5195E]/20 transition-all duration-200"
|
||||
aria-label={social.name}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Navigation Sections */}
|
||||
<FooterSection
|
||||
title="Explore"
|
||||
links={footerNavigation.Explore}
|
||||
delay={0.1}
|
||||
/>
|
||||
<FooterSection
|
||||
title="Services"
|
||||
links={footerNavigation.Services}
|
||||
delay={0.2}
|
||||
/>
|
||||
<FooterSection
|
||||
title="AI & ML"
|
||||
links={footerNavigation["AI & ML"]}
|
||||
delay={0.3}
|
||||
/>
|
||||
<FooterSection
|
||||
title="Solutions"
|
||||
links={footerNavigation.Solutions}
|
||||
delay={0.4}
|
||||
/>
|
||||
<FooterSection
|
||||
title="Resources"
|
||||
links={footerNavigation.Resources}
|
||||
delay={0.5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Newsletter Subscription Section */}
|
||||
<NewsletterSection />
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="border-t border-white/10"
|
||||
>
|
||||
<div className="container mx-auto px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col lg:flex-row justify-between items-center gap-6">
|
||||
<div className="text-[#CCCCCC] text-sm text-center lg:text-left">
|
||||
© 2024 Web Development Institute. All rights
|
||||
reserved.
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<a
|
||||
href="/privacy"
|
||||
className="text-[#CCCCCC] hover:text-white transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a
|
||||
href="/terms"
|
||||
className="text-[#CCCCCC] hover:text-white transition-colors"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
<a
|
||||
href="/cookies"
|
||||
className="text-[#CCCCCC] hover:text-white transition-colors"
|
||||
>
|
||||
Cookie Policy
|
||||
</a>
|
||||
<a
|
||||
href="/sitemap"
|
||||
className="text-[#CCCCCC] hover:text-white transition-colors"
|
||||
>
|
||||
Sitemap
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="text-[#CCCCCC] text-sm text-center lg:text-right">
|
||||
Engineered by WDI — because someone had to do it
|
||||
right. 💻
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
26
components/GridPattern.tsx
Normal file
26
components/GridPattern.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
export const GridPattern = ({ strokeDasharray = "4 2" }: { strokeDasharray?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className="absolute inset-0 h-full w-full opacity-20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="grid"
|
||||
width="40"
|
||||
height="40"
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<path
|
||||
d="M 40 0 L 0 0 0 40"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.3)"
|
||||
strokeWidth="1"
|
||||
strokeDasharray={strokeDasharray}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
83
components/HeroBanner.tsx
Normal file
83
components/HeroBanner.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Button } from "./ui/button";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
interface HeroBannerProps {
|
||||
category?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryCTA: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
secondaryCTA?: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function HeroBanner({
|
||||
category,
|
||||
title,
|
||||
description,
|
||||
primaryCTA,
|
||||
secondaryCTA
|
||||
}: HeroBannerProps) {
|
||||
return (
|
||||
<section className="relative py-20 lg:py-32 bg-[#0E0E0E] overflow-hidden">
|
||||
<GridPattern />
|
||||
|
||||
<div className="container mx-auto px-6 lg:px-8 relative z-10">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
{/* Category Badge */}
|
||||
{category && (
|
||||
<div className="inline-flex items-center rounded-full px-4 py-2 mb-8 bg-[#E5195E]/10 border border-[#E5195E]/20">
|
||||
<span className="text-[#E5195E] text-sm font-medium">
|
||||
{category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Title */}
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-semibold tracking-tight text-white mb-6">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-lg lg:text-xl text-gray-400 mb-10 max-w-3xl mx-auto leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-[#E5195E] to-[#C41653] hover:from-[#C41653] hover:to-[#A31348] text-white font-semibold px-8 py-4 h-auto text-base"
|
||||
onClick={() => navigateTo(primaryCTA.href)}
|
||||
>
|
||||
{primaryCTA.text}
|
||||
</Button>
|
||||
|
||||
{secondaryCTA && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="bg-white/5 hover:bg-white/10 text-white border-white/20 hover:border-white/30 font-medium px-8 py-4 h-auto text-base"
|
||||
onClick={() => navigateTo(secondaryCTA.href)}
|
||||
>
|
||||
{secondaryCTA.text}
|
||||
<ChevronRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-gradient-to-r from-[#E5195E]/10 to-purple-500/10 rounded-full blur-3xl animate-pulse opacity-60"></div>
|
||||
<div className="absolute top-3/4 right-1/4 w-24 h-24 bg-gradient-to-r from-blue-500/10 to-cyan-500/10 rounded-full blur-2xl animate-pulse delay-1000 opacity-60"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-20 h-20 bg-gradient-to-r from-green-500/10 to-emerald-500/10 rounded-full blur-2xl animate-pulse delay-2000 opacity-60"></div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
90
components/HeroSection.tsx
Normal file
90
components/HeroSection.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Button } from "./ui/button";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { SplineFallback } from "./SplineFallback";
|
||||
import { Calendar, Briefcase } from "lucide-react";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
export function HeroSection() {
|
||||
return (
|
||||
<section id="hero" className="relative lg:min-h-[85vh] flex items-center pt-20">
|
||||
<GridPattern />
|
||||
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<div className="flex flex-col-reverse lg:flex-row items-center gap-12 w-full py-24 relative z-10">
|
||||
<div className="w-full lg:w-1/2">
|
||||
{/* Animated Badge */}
|
||||
<div className="group relative inline-flex items-center rounded-full px-4 py-1.5 shadow-[inset_0_-8px_10px_#8fdfff1f] transition-shadow duration-500 ease-out hover:shadow-[inset_0_-5px_10px_#8fdfff3f] mb-6">
|
||||
<span
|
||||
className="absolute inset-0 block h-full w-full animate-gradient rounded-[inherit] bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:300%_100%] p-[1px]"
|
||||
style={{
|
||||
WebkitMask: "linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0)",
|
||||
WebkitMaskComposite: "destination-out",
|
||||
maskComposite: "subtract",
|
||||
}}
|
||||
/>
|
||||
<span className="relative z-10 flex items-center text-sm font-medium">
|
||||
🎉
|
||||
<span aria-hidden="true" className="mx-2 h-4 w-px shrink-0 bg-neutral-500" />
|
||||
<span className="bg-clip-text text-transparent bg-[linear-gradient(90deg,#ffaa40_0%,#9c40ff_50%,#ffaa40_100%)] bg-[length:200%_100%] animate-[gradientMove_6s_ease_infinite]">
|
||||
25+ Years Of Industry Expertise
|
||||
</span>
|
||||
<svg
|
||||
className="ml-1 w-4 h-4 stroke-neutral-500 transition-transform duration-300 group-hover:translate-x-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight text-white max-w-3xl">
|
||||
Architecting Digital Success for Startups & Enterprises
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 max-w-2xl text-lg text-gray-400">
|
||||
We design and build secure, AI-powered apps and software tailored for scale, speed, and user engagement.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex flex-col sm:flex-row gap-3">
|
||||
<Button size="lg" className="whitespace-nowrap" onClick={() => navigateTo('/contact')}>
|
||||
<Calendar className="w-4 h-4" />
|
||||
Book a Free Consultation
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" size="lg" className="whitespace-nowrap" onClick={() => navigateTo('/services')}>
|
||||
<Briefcase className="w-4 h-4" />
|
||||
Explore Services
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2 h-[320px] md:h-[480px] lg:h-[560px] shrink-0 relative">
|
||||
{/* Animated Background Elements */}
|
||||
<div className="absolute inset-0 overflow-hidden rounded-xl">
|
||||
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-gradient-to-r from-[#E5195E]/20 to-purple-500/20 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div className="absolute top-3/4 right-1/4 w-24 h-24 bg-gradient-to-r from-blue-500/20 to-cyan-500/20 rounded-full blur-2xl animate-pulse delay-1000"></div>
|
||||
<div className="absolute bottom-1/4 left-1/3 w-20 h-20 bg-gradient-to-r from-green-500/20 to-emerald-500/20 rounded-full blur-2xl animate-pulse delay-2000"></div>
|
||||
</div>
|
||||
|
||||
{/* Interactive 3D-like Animation */}
|
||||
<div className="relative w-full h-full rounded-xl overflow-hidden border border-gray-800/50 bg-gradient-to-br from-gray-900/50 to-gray-800/30 backdrop-blur-sm">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/5 to-purple-500/5 rounded-xl"></div>
|
||||
<div className="relative z-10 w-full h-full">
|
||||
<SplineFallback />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Elements */}
|
||||
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce">
|
||||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
95
components/HorizontalTagScroller.tsx
Normal file
95
components/HorizontalTagScroller.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
CreditCard,
|
||||
Heart,
|
||||
ShoppingCart,
|
||||
GraduationCap,
|
||||
Truck,
|
||||
Video,
|
||||
Building,
|
||||
Plane,
|
||||
Factory,
|
||||
Wheat,
|
||||
Gamepad2,
|
||||
Cloud
|
||||
} from "lucide-react";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
const industries = [
|
||||
// First row
|
||||
{ name: "FinTech", icon: CreditCard },
|
||||
{ name: "HealthTech", icon: Heart },
|
||||
{ name: "eCommerce", icon: ShoppingCart },
|
||||
{ name: "EdTech", icon: GraduationCap },
|
||||
// Second row
|
||||
{ name: "Logistics", icon: Truck },
|
||||
{ name: "Media & OTT", icon: Video },
|
||||
{ name: "Real Estate", icon: Building },
|
||||
{ name: "Travel", icon: Plane },
|
||||
// Third row (we'll make it 3x4 instead to fit all 12)
|
||||
{ name: "Manufacturing", icon: Factory },
|
||||
{ name: "AgriTech", icon: Wheat },
|
||||
{ name: "Gaming", icon: Gamepad2 },
|
||||
{ name: "SaaS", icon: Cloud }
|
||||
];
|
||||
|
||||
const IndustryCard = ({ industry, index }: {
|
||||
industry: { name: string; icon: any };
|
||||
index: number;
|
||||
}) => {
|
||||
const Icon = industry.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ y: -5, scale: 1.02 }}
|
||||
className="group p-6 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 hover:border-[#E5195E]/50 transition-all duration-300 cursor-pointer text-center"
|
||||
>
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-xl bg-[#E5195E]/10 flex items-center justify-center group-hover:scale-110 group-hover:bg-[#E5195E]/20 transition-all duration-300">
|
||||
<Icon className="w-8 h-8 text-[#E5195E]" />
|
||||
</div>
|
||||
<h3 className="text-white font-medium text-lg group-hover:text-[#E5195E] transition-colors duration-300">
|
||||
{industry.name}
|
||||
</h3>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HorizontalTagScroller = () => {
|
||||
return (
|
||||
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
|
||||
Tailored Solutions for Your Industry
|
||||
</h2>
|
||||
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
|
||||
We serve diverse industries with specialized expertise and domain knowledge
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* 4x3 Grid for larger screens, 2x6 for tablets, 1x12 for mobile */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
|
||||
{industries.map((industry, index) => (
|
||||
<IndustryCard
|
||||
key={industry.name}
|
||||
industry={industry}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
214
components/InlineCTA.tsx
Normal file
214
components/InlineCTA.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Lightbulb, Clock } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
export const InlineCTA = () => {
|
||||
return (
|
||||
<section className="relative py-20 overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="max-w-4xl mx-auto text-center"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<motion.div
|
||||
className="w-20 h-20 mx-auto mb-6 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 flex items-center justify-center relative"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* Animated glow effect */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-accent/20"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.3, 0.6, 0.3],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Pulsing ring */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full border border-accent/30"
|
||||
animate={{
|
||||
scale: [1, 1.1, 1],
|
||||
opacity: [0.5, 0.8, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0.5
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Main icon with subtle animation */}
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: [0, 5, -5, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 3,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
>
|
||||
<Lightbulb className="w-10 h-10 text-accent relative z-10" />
|
||||
</motion.div>
|
||||
|
||||
{/* Sparkle effects */}
|
||||
<motion.div
|
||||
className="absolute -top-1 -right-1 w-2 h-2 bg-accent rounded-full"
|
||||
animate={{
|
||||
scale: [0, 1.2, 0],
|
||||
opacity: [0, 1, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0.3
|
||||
}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute -bottom-1 -left-1 w-1.5 h-1.5 bg-accent rounded-full"
|
||||
animate={{
|
||||
scale: [0, 1, 0],
|
||||
opacity: [0, 0.8, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2.2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 0.8
|
||||
}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute top-2 -left-2 w-1 h-1 bg-accent rounded-full"
|
||||
animate={{
|
||||
scale: [0, 1.5, 0],
|
||||
opacity: [0, 0.6, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 1.2
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.h2
|
||||
className="text-3xl lg:text-5xl font-semibold text-foreground mb-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
Have an Idea? Let's Talk.
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
Get clarity, timelines, and answers within 24 hours.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-4 text-lg border-0 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
onClick={() => navigateTo('/contact')}
|
||||
>
|
||||
<motion.div
|
||||
animate={{
|
||||
rotate: [0, 10, -10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<Lightbulb className="w-5 h-5" />
|
||||
</motion.div>
|
||||
Request a Proposal
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span className="text-sm">24-hour response guarantee</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="grid grid-cols-3 gap-8 max-w-2xl mx-auto text-center"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.7 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="text-2xl font-bold text-foreground">15min</div>
|
||||
<div className="text-sm text-muted-foreground">Quick Discovery Call</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="text-2xl font-bold text-foreground">24hrs</div>
|
||||
<div className="text-sm text-muted-foreground">Detailed Proposal</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.9 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<div className="text-2xl font-bold text-foreground">48hrs</div>
|
||||
<div className="text-sm text-muted-foreground">Project Kickoff</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
843
components/Navigation.tsx
Normal file
843
components/Navigation.tsx
Normal file
@@ -0,0 +1,843 @@
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
ChevronDown,
|
||||
Menu,
|
||||
X,
|
||||
Code,
|
||||
Smartphone,
|
||||
Globe,
|
||||
Palette,
|
||||
Brain,
|
||||
Users,
|
||||
Building2,
|
||||
Monitor,
|
||||
ShoppingCart,
|
||||
Server,
|
||||
Wrench,
|
||||
Lightbulb,
|
||||
Database,
|
||||
Eye,
|
||||
MessageSquare,
|
||||
Target,
|
||||
BarChart3,
|
||||
Zap,
|
||||
Rocket,
|
||||
Shield,
|
||||
Cog,
|
||||
HeartHandshake,
|
||||
GraduationCap,
|
||||
Stethoscope,
|
||||
ShoppingBag,
|
||||
Truck,
|
||||
Gamepad2,
|
||||
Factory,
|
||||
DollarSign,
|
||||
Home,
|
||||
BookOpen,
|
||||
Users2,
|
||||
Code2,
|
||||
Laptop,
|
||||
Paintbrush,
|
||||
Bot,
|
||||
RefreshCw,
|
||||
Info,
|
||||
Clock,
|
||||
Award,
|
||||
Briefcase,
|
||||
Heart,
|
||||
Newspaper,
|
||||
FileText,
|
||||
Star,
|
||||
HelpCircle,
|
||||
Mail,
|
||||
FileCheck,
|
||||
Phone,
|
||||
MapPin,
|
||||
Headphones,
|
||||
UserPlus,
|
||||
Apple,
|
||||
GitMerge,
|
||||
Gauge,
|
||||
Chrome,
|
||||
Watch,
|
||||
Cloud,
|
||||
CloudCog,
|
||||
Link,
|
||||
PenTool,
|
||||
MousePointer2,
|
||||
TestTube,
|
||||
PlayCircle,
|
||||
Search,
|
||||
Workflow,
|
||||
Sparkles,
|
||||
Wand2,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
Calculator,
|
||||
Calendar,
|
||||
FileEdit
|
||||
} from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import BlackLogo14 from "../imports/BlackLogo14";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
const navigationData = {
|
||||
main_navigation: [
|
||||
"Services",
|
||||
"AI & ML",
|
||||
"Solutions",
|
||||
"Industries",
|
||||
"Hire Talent",
|
||||
"Company",
|
||||
"Resources"
|
||||
],
|
||||
services: [
|
||||
{
|
||||
category: "Mobile App Development",
|
||||
icon: Smartphone,
|
||||
href: "/services/mobile-app-development",
|
||||
sub_services: [
|
||||
{ name: "iOS App Development", href: "/services/ios-app-development" },
|
||||
{ name: "Android App Development", href: "/services/android-app-development" },
|
||||
{ name: "Cross-Platform App Development", href: "/services/cross-platform-app-development" },
|
||||
{ name: "Native App Development", href: "/services/native-app-development" },
|
||||
{ name: "Progressive Web Apps (PWAs)", href: "/services/pwa-development" },
|
||||
{ name: "App Development for Wearables & Devices", href: "/services/wearable-device-development" }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "Web & Cloud Solutions",
|
||||
icon: Globe,
|
||||
href: "/web-cloud",
|
||||
sub_services: [
|
||||
{ name: "Custom Web Application Development", href: "/services/custom-web-app-development" },
|
||||
{ name: "SaaS Product Engineering", href: "/services/saas-product-engineering" },
|
||||
{ name: "eCommerce Platforms", href: "/services/ecommerce-platforms" },
|
||||
{ name: "Admin Panels & Dashboards", href: "/services/admin-panels-dashboards" },
|
||||
{ name: "API & Backend Development", href: "/services/api-backend-development" }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "Software Engineering",
|
||||
icon: Code2,
|
||||
href: "/software-engineering",
|
||||
sub_services: [
|
||||
{ name: "Enterprise Software Solutions", href: "/services/enterprise-software-solutions" },
|
||||
{ name: "System Architecture & DevOps", href: "/services/system-architecture-devops" },
|
||||
{ name: "Third-Party Integrations", href: "/services/third-party-integrations" },
|
||||
{ name: "Product Modernization", href: "/services/product-modernization" }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "Design & Experience",
|
||||
icon: Paintbrush,
|
||||
href: "/design-experience",
|
||||
sub_services: [
|
||||
{ name: "UI/UX Design", href: "/services/ui-ux-design" },
|
||||
{ name: "Clickable Prototypes", href: "/services/clickable-prototypes" },
|
||||
{ name: "Design Thinking Workshops", href: "/services/design-thinking-workshops" },
|
||||
{ name: "User Research & Testing", href: "/services/user-research-testing" }
|
||||
]
|
||||
}
|
||||
],
|
||||
ai_data_intelligence: [
|
||||
{
|
||||
category: "Artificial Intelligence Services",
|
||||
icon: Bot,
|
||||
href: "/artificial-intelligence",
|
||||
sub_services: [
|
||||
{ name: "AI Strategy & Consulting", href: "/services/ai-strategy-consulting" },
|
||||
{ name: "AI-Powered Automation & Workflows", href: "/services/ai-automation-workflows" },
|
||||
{ name: "AI Integration into Digital Products", href: "/services/ai-integration-digital-products" },
|
||||
{ name: "Gen AI Integration into Digital Products", href: "/services/gen-ai-integration-digital-products" },
|
||||
{ name: "AI Chatbots & Virtual Assistants", href: "/services/ai-chatbots-virtual-assistants" },
|
||||
{ name: "AI Model Deployment & Maintenance", href: "/services/ai-model-deployment-mlops" }
|
||||
]
|
||||
},
|
||||
{
|
||||
category: "Machine Learning Solutions",
|
||||
icon: Brain,
|
||||
href: "/machine-learning",
|
||||
sub_services: [
|
||||
{ name: "Custom ML Model Development", href: "/services/custom-ml-model-development" },
|
||||
{ name: "Predictive Analytics & Forecasting", href: "/services/predictive-analytics-forecasting" },
|
||||
{ name: "Computer Vision Applications", href: "/services/computer-vision-applications" },
|
||||
{ name: "NLP & Text Analytics", href: "/services/nlp-text-analytics" },
|
||||
{ name: "Recommendation Engines", href: "/services/recommendation-engines" }
|
||||
]
|
||||
}
|
||||
],
|
||||
solutions: [
|
||||
{ text: "Digital Product Development", icon: Rocket, href: "/solutions/digital-product-development" },
|
||||
{ text: "MVP & Startup Launch Packages", icon: Zap, href: "/solutions/mvp-startup-launch-packages" },
|
||||
{ text: "Legacy System Rebuilds", icon: RefreshCw, href: "/solutions/legacy-system-rebuilds" },
|
||||
{ text: "Dedicated Offshore Development Centers (ODC)", icon: Building2, href: "/solutions/dedicated-offshore-odc" },
|
||||
{ text: "Business Process Automation", icon: Cog, href: "/solutions/business-process-automation" },
|
||||
{ text: "Compliance-Ready Systems (HIPAA, GDPR, etc.)", icon: Shield, href: "/solutions/compliance-ready-systems" }
|
||||
],
|
||||
industries: [
|
||||
{
|
||||
group: "Financial Services",
|
||||
icon: DollarSign,
|
||||
items: [
|
||||
{ name: "FinTech & Banking Apps", href: "/industries/fintech-banking-apps" },
|
||||
{ name: "WealthTech Platforms", href: "/industries/financial-services/wealthtech-platforms" },
|
||||
{ name: "Real Estate Tech", href: "/industries/financial-services/real-estate-tech" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Healthcare & Wellness",
|
||||
icon: Stethoscope,
|
||||
items: [
|
||||
{ name: "HealthTech Applications", href: "/industries/healthcare/healthtech-applications" },
|
||||
{ name: "Medical Compliance Solutions", href: "/industries/healthcare/medical-compliance-solutions" },
|
||||
{ name: "Fitness & Wellness Platforms", href: "/industries/healthcare/fitness-wellness-platforms" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Learning & Education",
|
||||
icon: GraduationCap,
|
||||
items: [
|
||||
{ name: "EdTech Platforms", href: "/industries/education/edtech-platforms" },
|
||||
{ name: "Virtual Classrooms & LMS", href: "/industries/education/virtual-classrooms-lms" },
|
||||
{ name: "Microlearning Apps", href: "/industries/education/microlearning-apps" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Commerce & Consumer",
|
||||
icon: ShoppingBag,
|
||||
items: [
|
||||
{ name: "eCommerce & Marketplaces", href: "/industries/commerce/ecommerce-marketplaces" },
|
||||
{ name: "Food Ordering & Delivery", href: "/industries/commerce/food-ordering-delivery" },
|
||||
{ name: "Travel & Booking Systems", href: "/industries/commerce/travel-booking-systems" },
|
||||
{ name: "Event & Ticketing Solutions", href: "/industries/commerce/event-ticketing-solutions" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Media & Community",
|
||||
icon: Gamepad2,
|
||||
items: [
|
||||
{ name: "OTT & Streaming Apps", href: "/industries/media/ott-streaming-apps" },
|
||||
{ name: "Social Platforms & Networks", href: "/industries/media/social-platforms-networks" },
|
||||
{ name: "Sports & Fan Engagement", href: "/industries/media/sports-fan-engagement" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Mobility & Logistics",
|
||||
icon: Truck,
|
||||
items: [
|
||||
{ name: "Transportation Apps", href: "/industries/mobility/transportation-apps" },
|
||||
{ name: "On-Demand Services", href: "/industries/mobility/on-demand-services" },
|
||||
{ name: "Supply Chain & Fleet Management", href: "/industries/mobility/supply-chain-fleet-management" }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Industrial & Emerging Tech",
|
||||
icon: Factory,
|
||||
items: [
|
||||
{ name: "Manufacturing Automation", href: "/industries/industrial/manufacturing-automation" },
|
||||
{ name: "AgriTech Platforms", href: "/industries/industrial/agritech-platforms" },
|
||||
{ name: "Oil & Gas Monitoring Systems", href: "/industries/industrial/oil-gas-monitoring-systems" }
|
||||
]
|
||||
}
|
||||
],
|
||||
hire_talent: [
|
||||
{ text: "Hire Mobile App Developers", icon: Smartphone, href: "/hire-talent/mobile-app-developers" },
|
||||
{ text: "Hire Full Stack Developers", icon: Code, href: "/hire-talent/full-stack-developers" },
|
||||
{ text: "Hire Frontend Developers", icon: Monitor, href: "/hire-talent/frontend-developers" },
|
||||
{ text: "Hire Backend Developers", icon: Database, href: "/hire-talent/backend-developers" },
|
||||
{ text: "Hire UI/UX Designers", icon: Palette, href: "/hire-talent/ui-ux-designers" },
|
||||
{ text: "Hire QA Engineers", icon: TestTube, href: "/hire-talent/qa-engineers" },
|
||||
{ text: "Dedicated Development Teams", icon: Users, href: "/dedicated-development-teams" },
|
||||
{ text: "Engagement Models", icon: Settings, href: "/engagement-models" },
|
||||
{ text: "Team Augmentation Services", icon: Zap, href: "/team-augmentation-services" }
|
||||
],
|
||||
company: [
|
||||
{ text: "About WDI", icon: Info, href: "/company/about-wdi" },
|
||||
{ text: "Our History", icon: Clock, href: "/company/our-history" },
|
||||
{ text: "Leadership Team", icon: Users2, href: "/company/leadership-team" },
|
||||
{ text: "Awards & Certifications", icon: Award, href: "/company/awards-certifications" },
|
||||
{ text: "Careers", icon: Briefcase, href: "/company/careers" },
|
||||
{ text: "Culture & Values", icon: Heart, href: "/company/culture-values" },
|
||||
{ text: "Press & Media", icon: Newspaper, href: "/company/press-media" }
|
||||
],
|
||||
resources: [
|
||||
{ text: "Articles", icon: BookOpen, href: "/resources/blog" },
|
||||
{ text: "Case Studies", icon: FileText, href: "/case-studies" },
|
||||
{ text: "Client Testimonials", icon: Star, href: "/resources/client-testimonials" },
|
||||
{ text: "Whitepapers & Insights", icon: FileCheck, href: "/resources/whitepapers-insights" },
|
||||
{ text: "FAQs", icon: HelpCircle, href: "/resources/faqs" }
|
||||
],
|
||||
contact: [
|
||||
{ text: "Contact Form", icon: Mail, href: "/contact" },
|
||||
{ text: "Request a Proposal", icon: FileCheck, href: "/contact/request-a-proposal" },
|
||||
{ text: "Schedule a Discovery Call", icon: Phone, href: "/contact/schedule-a-discovery-call" },
|
||||
{ text: "Office Locations", icon: MapPin, href: "/contact/office-locations" },
|
||||
{ text: "Client Support", icon: Headphones, href: "/contact/client-support" },
|
||||
{ text: "Send your CV to HR", icon: UserPlus, href: "/contact/send-your-cv" }
|
||||
]
|
||||
};
|
||||
|
||||
// CTA configurations for each mega menu type - UPDATED ALL TO LINK TO START A PROJECT PAGE
|
||||
const megaMenuCTAs = {
|
||||
Services: {
|
||||
title: "Development Quote",
|
||||
subtitle: "Get a custom quote for your project",
|
||||
buttonText: "Get Started",
|
||||
href: "/start-a-project",
|
||||
icon: Calculator
|
||||
},
|
||||
"AI & ML": {
|
||||
title: "AI Strategy Session",
|
||||
subtitle: "Discover AI opportunities for your business",
|
||||
buttonText: "Book Session",
|
||||
href: "/start-a-project",
|
||||
icon: Bot
|
||||
},
|
||||
Solutions: {
|
||||
title: "Solution Consultation",
|
||||
subtitle: "Find the perfect solution for your business",
|
||||
buttonText: "Consult Now",
|
||||
href: "/start-a-project",
|
||||
icon: Lightbulb
|
||||
},
|
||||
Industries: {
|
||||
title: "Industry Expertise",
|
||||
subtitle: "Learn how we transform your industry",
|
||||
buttonText: "Explore",
|
||||
href: "/start-a-project",
|
||||
icon: Building2
|
||||
},
|
||||
"Hire Talent": {
|
||||
title: "Team Assessment",
|
||||
subtitle: "Get matched with the right talent",
|
||||
buttonText: "Start Hiring",
|
||||
href: "/start-a-project",
|
||||
icon: Users
|
||||
},
|
||||
Company: {
|
||||
title: "Schedule a Call",
|
||||
subtitle: "Let's discuss your project requirements",
|
||||
buttonText: "Book Call",
|
||||
href: "/start-a-project",
|
||||
icon: Calendar
|
||||
},
|
||||
Resources: {
|
||||
title: "Free Consultation",
|
||||
subtitle: "Get expert insights for your project",
|
||||
buttonText: "Get Started",
|
||||
href: "/start-a-project",
|
||||
icon: FileEdit
|
||||
},
|
||||
Contact: {
|
||||
title: "Start Your Project",
|
||||
subtitle: "Ready to bring your idea to life?",
|
||||
buttonText: "Get Started",
|
||||
href: "/start-a-project",
|
||||
icon: Rocket
|
||||
}
|
||||
};
|
||||
|
||||
// Horizontal CTA Component matching reference design
|
||||
const MegaMenuCTA = ({ type }: { type: string }) => {
|
||||
const cta = megaMenuCTAs[type as keyof typeof megaMenuCTAs];
|
||||
if (!cta) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
className="mt-8 p-6 bg-gradient-to-r from-gray-900/60 to-gray-800/60 backdrop-blur-sm border border-gray-700/30 rounded-2xl"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-semibold text-lg mb-1">{cta.title}</h3>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">{cta.subtitle}</p>
|
||||
</div>
|
||||
<div className="ml-6">
|
||||
<Button
|
||||
className="bg-gradient-to-r from-[#E5195E] to-[#C41653] hover:from-[#C41653] hover:to-[#A31348] text-white font-medium text-sm px-6 py-3 h-auto rounded-xl shadow-lg hover:shadow-xl transition-all duration-200 group"
|
||||
onClick={() => navigateTo(cta.href)}
|
||||
>
|
||||
{cta.buttonText}
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
interface MegaMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCancelClose: () => void;
|
||||
type: string;
|
||||
timeoutRef?: React.MutableRefObject<NodeJS.Timeout | undefined>;
|
||||
}
|
||||
|
||||
const MegaMenu = ({ isOpen, onClose, onCancelClose, type, timeoutRef }: MegaMenuProps) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const navigate = (path: string) => {
|
||||
navigateTo(path);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderServicesMenu = () => (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{navigationData.services.map((service, index) => {
|
||||
const Icon = service.icon;
|
||||
return (
|
||||
<div key={service.category} className="space-y-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-[#E5195E]/20 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-[#E5195E]" />
|
||||
</div>
|
||||
<h4
|
||||
className="font-semibold text-white text-sm cursor-pointer hover:text-[#E5195E] transition-colors"
|
||||
onClick={() => service.href && navigate(service.href)}
|
||||
>
|
||||
{service.category}
|
||||
</h4>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{service.sub_services.map((subService) => (
|
||||
<li key={subService.name}>
|
||||
<a
|
||||
href="#"
|
||||
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (subService.href) {
|
||||
navigate(subService.href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{subService.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MegaMenuCTA type="Services" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderAIMenu = () => (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{navigationData.ai_data_intelligence.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<div key={category.category} className="space-y-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-[#E5195E]/20 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-[#E5195E]" />
|
||||
</div>
|
||||
<h4
|
||||
className="font-semibold text-white text-lg cursor-pointer hover:text-[#E5195E] transition-colors"
|
||||
onClick={() => category.href && navigate(category.href)}
|
||||
>
|
||||
{category.category}
|
||||
</h4>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{category.sub_services.map((service) => (
|
||||
<li key={service.name}>
|
||||
<a
|
||||
href="#"
|
||||
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (service.href) {
|
||||
navigate(service.href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{service.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MegaMenuCTA type="AI & ML" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSolutionsMenu = () => (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{navigationData.solutions.map((solution) => {
|
||||
const Icon = solution.icon;
|
||||
return (
|
||||
<a
|
||||
key={solution.text}
|
||||
href="#"
|
||||
className="flex items-center gap-4 text-[#CCCCCC] hover:text-white transition-all duration-200 p-4 rounded-lg hover:bg-white/5 group"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (solution.href) {
|
||||
navigate(solution.href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Icon className="w-4 h-4 text-[#E5195E]" />
|
||||
</div>
|
||||
<span className="font-medium">{solution.text}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MegaMenuCTA type="Solutions" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderIndustriesMenu = () => (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{navigationData.industries.map((industry) => {
|
||||
const Icon = industry.icon;
|
||||
return (
|
||||
<div key={industry.group} className="space-y-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center">
|
||||
<Icon className="w-4 h-4 text-[#E5195E]" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-white text-sm border-b border-white/10 pb-2">
|
||||
{industry.group}
|
||||
</h4>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{industry.items.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href="#"
|
||||
className="text-[#CCCCCC] hover:text-white text-sm transition-colors duration-200 block py-1 hover:translate-x-1 transform"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (item.href) {
|
||||
navigate(item.href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MegaMenuCTA type="Industries" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderGenericMenu = (items: any[], menuType: string) => (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<a
|
||||
key={item.text}
|
||||
href="#"
|
||||
className="flex items-center gap-4 text-[#CCCCCC] hover:text-white transition-all duration-200 p-4 rounded-lg hover:bg-white/5 group"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (item.href) {
|
||||
navigate(item.href);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-[#E5195E]/20 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Icon className="w-4 h-4 text-[#E5195E]" />
|
||||
</div>
|
||||
<span className="font-medium">{item.text}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<MegaMenuCTA type={menuType} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const getMenuContent = () => {
|
||||
switch (type) {
|
||||
case 'Services':
|
||||
return renderServicesMenu();
|
||||
case 'AI & ML':
|
||||
return renderAIMenu();
|
||||
case 'Solutions':
|
||||
return renderSolutionsMenu();
|
||||
case 'Industries':
|
||||
return renderIndustriesMenu();
|
||||
case 'Hire Talent':
|
||||
return renderGenericMenu(navigationData.hire_talent, 'Hire Talent');
|
||||
case 'Company':
|
||||
return renderGenericMenu(navigationData.company, 'Company');
|
||||
case 'Resources':
|
||||
return renderGenericMenu(navigationData.resources, 'Resources');
|
||||
case 'Contact':
|
||||
return renderGenericMenu(navigationData.contact, 'Contact');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute top-full left-0 w-full bg-[#121212] backdrop-blur-xl border-t border-white/10 shadow-xl z-50 nav-mega-menu"
|
||||
style={{ minHeight: '400px' }}
|
||||
onMouseEnter={onCancelClose}
|
||||
onMouseLeave={onClose}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#121212] to-[#0E0E0E] opacity-95" />
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 1px 1px, rgba(255,255,255,0.1) 1px, transparent 0)`,
|
||||
backgroundSize: '20px 20px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8 py-12">
|
||||
{getMenuContent()}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Navigation = () => {
|
||||
const [activeMenu, setActiveMenu] = useState<string | null>(null);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
|
||||
const navigate = (path: string) => {
|
||||
navigateTo(path);
|
||||
};
|
||||
|
||||
const openMenu = useCallback((item: string) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
if (['Services', 'AI & ML', 'Solutions', 'Industries', 'Hire Talent', 'Company', 'Resources'].includes(item)) {
|
||||
setActiveMenu(item);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActiveMenu(null);
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
const cancelClose = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNavItemMouseEnter = useCallback((item: string) => {
|
||||
cancelClose();
|
||||
openMenu(item);
|
||||
}, [cancelClose, openMenu]);
|
||||
|
||||
const handleNavItemMouseLeave = useCallback(() => {
|
||||
closeMenu();
|
||||
}, [closeMenu]);
|
||||
|
||||
const handleNavMouseLeave = useCallback((e: React.MouseEvent) => {
|
||||
const relatedTarget = e.relatedTarget as Element;
|
||||
if (!navRef.current?.contains(relatedTarget)) {
|
||||
closeMenu();
|
||||
}
|
||||
}, [closeMenu]);
|
||||
|
||||
const handleNavMouseEnter = useCallback(() => {
|
||||
cancelClose();
|
||||
}, [cancelClose]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const hasDropdown = (item: string) => {
|
||||
return ['Services', 'AI & ML', 'Solutions', 'Industries', 'Hire Talent', 'Company', 'Resources'].includes(item);
|
||||
};
|
||||
|
||||
// Function to get main category page route for navigation items
|
||||
const getMainCategoryRoute = (item: string) => {
|
||||
switch (item) {
|
||||
case 'Services':
|
||||
return '/services';
|
||||
case 'Company':
|
||||
return '/company';
|
||||
case 'Resources':
|
||||
return '/resources';
|
||||
case 'Contact':
|
||||
return '/contact';
|
||||
case 'Hire Talent':
|
||||
return '/hire-talent';
|
||||
case 'AI & ML':
|
||||
return '/artificial-intelligence';
|
||||
case 'Solutions':
|
||||
return '/solutions/digital-product-development'; // Default to first solution
|
||||
case 'Industries':
|
||||
return '/industries/fintech-banking-apps'; // Default to first industry
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={navRef}
|
||||
className="fixed top-0 left-0 right-0 z-50 bg-[#0E0E0E]/90 backdrop-blur-lg border-b border-white/10"
|
||||
onMouseLeave={handleNavMouseLeave}
|
||||
onMouseEnter={handleNavMouseEnter}
|
||||
>
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<div className="flex items-center">
|
||||
<a
|
||||
href="#"
|
||||
className="flex items-center"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate('/');
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10">
|
||||
<BlackLogo14 />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-6 xl:space-x-8">
|
||||
{navigationData.main_navigation.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="relative nav-dropdown-trigger"
|
||||
onMouseEnter={() => handleNavItemMouseEnter(item)}
|
||||
onMouseLeave={handleNavItemMouseLeave}
|
||||
>
|
||||
<a
|
||||
href={`#${item.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
className="flex items-center gap-1 text-[#CCCCCC] hover:text-white transition-colors duration-200 py-2 font-medium text-sm xl:text-base whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const route = getMainCategoryRoute(item);
|
||||
if (route) {
|
||||
navigate(route);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
{hasDropdown(item) && (
|
||||
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${activeMenu === item ? 'rotate-180' : ''}`} />
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
onClick={() => navigate('/start-a-project')}
|
||||
className="hidden lg:flex"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
className="lg:hidden text-[#CCCCCC] hover:text-white transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
>
|
||||
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mega Menu */}
|
||||
<AnimatePresence>
|
||||
{activeMenu && (
|
||||
<MegaMenu
|
||||
isOpen={true}
|
||||
onClose={closeMenu}
|
||||
onCancelClose={cancelClose}
|
||||
type={activeMenu}
|
||||
timeoutRef={timeoutRef}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="lg:hidden bg-[#121212] border-t border-white/10"
|
||||
>
|
||||
<div className="container mx-auto px-6 py-6">
|
||||
<div className="space-y-4">
|
||||
{navigationData.main_navigation.map((item) => (
|
||||
<a
|
||||
key={item}
|
||||
href="#"
|
||||
className="block text-[#CCCCCC] hover:text-white transition-colors py-2 font-medium"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const route = getMainCategoryRoute(item);
|
||||
if (route) {
|
||||
navigate(route);
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate('/start-a-project');
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full mt-4"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
280
components/ProcessSection.tsx
Normal file
280
components/ProcessSection.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
import { ArrowRight, FileText, Users, CheckCircle, Rocket, Search, Code, Palette, Monitor } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "step-1",
|
||||
title: "1. Define Scope",
|
||||
description: "We begin by gathering all project inputs, defining clear goals, creating technical documentation, and aligning on expectations.",
|
||||
visual: {
|
||||
type: "icon_or_doc_mockup",
|
||||
style: "minimal_ui"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "step-2",
|
||||
title: "2. Design UI/UX",
|
||||
description: "Our designers craft user-centric interfaces in Figma with clickable flows, visual systems, and detailed wireframes for all screens.",
|
||||
tags: [
|
||||
{ label: "Wireframes", color: "#6366F1" },
|
||||
{ label: "Prototyping", color: "#10B981" },
|
||||
{ label: "UI System", color: "#F59E0B" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "step-3",
|
||||
title: "3. Develop with Agile Sprints",
|
||||
description: "We use Agile sprints to turn designs into production-ready code, with continuous integration and weekly builds.",
|
||||
tags: [
|
||||
{ label: "Frontend", color: "#3B82F6" },
|
||||
{ label: "Backend", color: "#0EA5E9" },
|
||||
{ label: "APIs", color: "#8B5CF6" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "step-4",
|
||||
title: "4. Test, Launch & Scale",
|
||||
description: "After QA and UAT, we help launch confidently. Then we monitor, iterate, and scale your product to grow with your users.",
|
||||
chat_simulation: [
|
||||
{ from: "You", text: "Are we ready to go live?" },
|
||||
{ from: "Team", text: "Yes! Final tests passed 🚀" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Chat simulation component - Compact version
|
||||
const ChatSimulation = ({ messages }: { messages: Array<{ from: string; text: string }> }) => {
|
||||
return (
|
||||
<div className="space-y-2 p-3 bg-background rounded-lg border border-border">
|
||||
{messages.map((message, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: message.from === "You" ? -20 : 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className={`flex ${message.from === "You" ? "justify-start" : "justify-end"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] px-3 py-1.5 rounded-lg ${
|
||||
message.from === "You"
|
||||
? "bg-muted border border-border text-foreground"
|
||||
: "bg-accent text-accent-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-medium mb-1 opacity-70">{message.from}</div>
|
||||
<div className="text-xs">{message.text}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Document mockup component - Compact version
|
||||
const DocumentMockup = () => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="w-full bg-background rounded-lg border border-border p-4">
|
||||
<div className="space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-accent" />
|
||||
<span className="text-xs font-medium text-foreground">Project Scope</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Draft v1.2</div>
|
||||
</div>
|
||||
|
||||
{/* Document sections */}
|
||||
<div className="space-y-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
whileInView={{ opacity: 1, width: "100%" }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="space-y-1.5"
|
||||
>
|
||||
<div className="h-1.5 bg-muted rounded w-3/4"></div>
|
||||
<div className="h-1.5 bg-muted/60 rounded w-full"></div>
|
||||
<div className="h-1.5 bg-muted/60 rounded w-5/6"></div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3 text-green-500" />
|
||||
<span className="text-muted-foreground">Requirements</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.6 }}
|
||||
viewport={{ once: true }}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-muted rounded text-xs"
|
||||
>
|
||||
<Search className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-muted-foreground">Research</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Process step card component
|
||||
const ProcessCard = ({ step, index }: { step: typeof steps[0]; index: number }) => {
|
||||
const cardRef = useRef(null);
|
||||
|
||||
const renderContent = () => {
|
||||
if (step.visual?.type === "icon_or_doc_mockup") {
|
||||
return <DocumentMockup />;
|
||||
}
|
||||
|
||||
if (step.chat_simulation) {
|
||||
return <ChatSimulation messages={step.chat_simulation} />;
|
||||
}
|
||||
|
||||
if (step.tags) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{step.tags.map((tag, tagIndex) => (
|
||||
<motion.div
|
||||
key={tagIndex}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
whileInView={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.4, delay: tagIndex * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Badge
|
||||
className="text-white border-0 px-3 py-1 text-xs font-medium rounded-lg"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
>
|
||||
{tag.label}
|
||||
</Badge>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={cardRef}
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: index * 0.15 }}
|
||||
viewport={{ once: true, margin: "-50px" }}
|
||||
className="bg-card rounded-lg border border-border hover:border-border/80 transition-all duration-300 overflow-hidden group hover:shadow-lg"
|
||||
>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="space-y-3">
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-foreground leading-tight text-lg"
|
||||
>
|
||||
{step.title}
|
||||
</motion.h3>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 + 0.3 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-muted-foreground leading-relaxed text-base"
|
||||
>
|
||||
{step.description}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.15 + 0.4 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
{renderContent()}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProcessSection = () => {
|
||||
const titleRef = useRef(null);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden py-20 bg-background">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Title Section */}
|
||||
<div ref={titleRef} className="text-center mb-16">
|
||||
<motion.h2
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-4xl lg:text-5xl font-semibold text-foreground mb-4"
|
||||
>
|
||||
How We Turn an Idea into a{" "}
|
||||
<span className="text-accent">Scalable Product</span>
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-muted-foreground text-xl max-w-2xl mx-auto"
|
||||
>
|
||||
Our proven process transforms your vision into reality through strategic planning,
|
||||
thoughtful design, and expert engineering—every step of the way.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Cards Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
||||
{steps.map((step, index) => (
|
||||
<ProcessCard key={step.id} step={step} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-16"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-accent hover:bg-accent/90 text-accent-foreground border-0 rounded-lg px-8 py-4 group text-lg"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
Start Your Project Today
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
169
components/ResourceCards.tsx
Normal file
169
components/ResourceCards.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, Calendar, Clock } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { ImageWithFallback } from "./figma/ImageWithFallback";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
const resources = [
|
||||
{
|
||||
title: "UX review presentations",
|
||||
excerpt: "How do you create compelling presentations that wow clients, and actually close projects and deals? Here are the key insights that will elevate your game.",
|
||||
readTime: "8 min read",
|
||||
date: "Dec 15, 2024",
|
||||
image: "https://images.unsplash.com/photo-1560472355-536de3962603?w=800&h=400&fit=crop&auto=format",
|
||||
author: {
|
||||
name: "Olivia Rhye",
|
||||
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=150&h=150&fit=crop&crop=face&auto=format"
|
||||
},
|
||||
category: "Design",
|
||||
slug: "ux-review-presentations"
|
||||
},
|
||||
{
|
||||
title: "Migrating to Linear 101",
|
||||
excerpt: "Linear helps streamline software projects, sprints, tasks, and bug tracking. Here's how to get started and make the most of it.",
|
||||
readTime: "6 min read",
|
||||
date: "Dec 10, 2024",
|
||||
image: "https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&h=400&fit=crop&auto=format",
|
||||
author: {
|
||||
name: "Phoenix Baker",
|
||||
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face&auto=format"
|
||||
},
|
||||
category: "Software Engineering",
|
||||
slug: "migrating-to-linear-101"
|
||||
},
|
||||
{
|
||||
title: "Building your API Stack",
|
||||
excerpt: "The rise of RESTful APIs has been met by a rise in tools for creating, testing, and managing them. Here are the best practices for API development.",
|
||||
readTime: "12 min read",
|
||||
date: "Dec 5, 2024",
|
||||
image: "https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?w=800&h=400&fit=crop&auto=format",
|
||||
author: {
|
||||
name: "Lana Steiner",
|
||||
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face&auto=format"
|
||||
},
|
||||
category: "Software Engineering",
|
||||
slug: "building-your-api-stack"
|
||||
}
|
||||
];
|
||||
|
||||
const ResourceCard = ({ resource, index }: { resource: typeof resources[0]; index: number }) => {
|
||||
return (
|
||||
<motion.article
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="group bg-card rounded-lg border border-border overflow-hidden hover:border-border/80 transition-all duration-300 hover:shadow-lg cursor-pointer"
|
||||
onClick={() => navigateTo(`/insights/${resource.slug}`)}
|
||||
>
|
||||
{/* Image */}
|
||||
<div className="aspect-[16/9] overflow-hidden relative">
|
||||
<ImageWithFallback
|
||||
src={resource.image}
|
||||
alt={resource.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
{/* Capsule Tag */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className="px-3 py-1.5 bg-background/90 backdrop-blur-sm text-foreground text-xs font-medium rounded-full border border-border/50">
|
||||
{resource.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Date and Read Time */}
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{resource.date}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{resource.readTime}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-foreground font-medium leading-tight group-hover:text-accent transition-colors">
|
||||
{resource.title}
|
||||
</h3>
|
||||
|
||||
{/* Excerpt */}
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
{resource.excerpt}
|
||||
</p>
|
||||
|
||||
{/* Author */}
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-border">
|
||||
<ImageWithFallback
|
||||
src={resource.author.avatar}
|
||||
alt={resource.author.name}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-foreground text-sm">
|
||||
{resource.author.name}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-accent hover:text-accent-foreground hover:bg-accent/10 p-2 h-auto group-hover:translate-x-1 transition-transform"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigateTo(`/insights/${resource.slug}`);
|
||||
}}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.article>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceCards = () => {
|
||||
return (
|
||||
<section className="relative py-20 overflow-hidden">
|
||||
<div className="container mx-auto px-6 lg:px-8">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-foreground mb-4">
|
||||
Insights for Founders & Product Leaders
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
Learn from our experience building 200+ digital products. Practical insights, real case studies, and actionable strategies.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Resource Cards Grid */}
|
||||
<div className="grid lg:grid-cols-3 gap-8 mb-12 max-w-7xl mx-auto">
|
||||
{resources.map((resource, index) => (
|
||||
<ResourceCard key={resource.title} resource={resource} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center"
|
||||
>
|
||||
<Button className="bg-accent hover:bg-accent/90 text-accent-foreground border-0 rounded-lg px-6 py-3" onClick={() => navigateTo('/resources')}>
|
||||
View All Resources <ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
420
components/ScrollParallaxProcess.tsx
Normal file
420
components/ScrollParallaxProcess.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
import { motion, useScroll, useTransform, useSpring } from "framer-motion";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { FileText, Figma, Code, Rocket, ArrowRight, Smartphone, Monitor, Palette, Database, Cloud, CheckCircle } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: "discovery",
|
||||
title: "Define the Scope",
|
||||
subtext: "We begin by understanding your vision, identifying key problems, and drafting a well-defined scope with clear goals and deliverables.",
|
||||
icon: FileText,
|
||||
visual: "workspace_parallax",
|
||||
color: "from-blue-500/20 to-cyan-500/20"
|
||||
},
|
||||
{
|
||||
id: "design",
|
||||
title: "Designing the Experience",
|
||||
subtext: "Our designers craft intuitive and stunning user interfaces in Figma, turning requirements into clickable, user-first prototypes.",
|
||||
icon: Figma,
|
||||
visual: "figma_canvas_animation",
|
||||
color: "from-purple-500/20 to-pink-500/20"
|
||||
},
|
||||
{
|
||||
id: "development",
|
||||
title: "Engineering the Solution",
|
||||
subtext: "We bring designs to life with clean, scalable code—choosing the right tech stack to ensure performance and longevity.",
|
||||
icon: Code,
|
||||
visual: "code_editor_animation",
|
||||
color: "from-green-500/20 to-emerald-500/20"
|
||||
},
|
||||
{
|
||||
id: "launch",
|
||||
title: "Launch & Beyond",
|
||||
subtext: "We ship your product with confidence—ensuring QA, deployment, and post-launch support to keep it growing.",
|
||||
icon: Rocket,
|
||||
visual: "launch_animation",
|
||||
color: "from-orange-500/20 to-red-500/20"
|
||||
}
|
||||
];
|
||||
|
||||
const WorkspaceVisual = ({ inView }: { inView: boolean }) => {
|
||||
return (
|
||||
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-blue-500/10 to-cyan-500/10 p-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Floating Documents */}
|
||||
<motion.div
|
||||
animate={inView ? { x: [-10, 10, -10], y: [-5, 5, -5] } : {}}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute top-4 left-4 w-12 h-16 bg-white/10 rounded shadow-lg flex items-center justify-center"
|
||||
>
|
||||
<FileText className="w-6 h-6 text-blue-400" />
|
||||
</motion.div>
|
||||
|
||||
{/* Sticky Notes */}
|
||||
<motion.div
|
||||
animate={inView ? { rotate: [-2, 2, -2] } : {}}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute top-16 right-8 w-16 h-16 bg-yellow-400/20 rounded-sm p-2"
|
||||
>
|
||||
<div className="w-full h-1 bg-yellow-400/40 rounded mb-1"></div>
|
||||
<div className="w-3/4 h-1 bg-yellow-400/30 rounded mb-1"></div>
|
||||
<div className="w-1/2 h-1 bg-yellow-400/20 rounded"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Pointer Highlighting */}
|
||||
<motion.div
|
||||
animate={inView ? { x: [0, 100, 0] } : {}}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
className="absolute bottom-8 left-8 w-2 h-2 bg-accent rounded-full"
|
||||
>
|
||||
<div className="absolute -top-1 -left-1 w-4 h-4 border-2 border-accent rounded-full animate-pulse"></div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FigmaCanvasVisual = ({ inView }: { inView: boolean }) => {
|
||||
return (
|
||||
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-purple-500/10 to-pink-500/10 p-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={inView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* UI Frames */}
|
||||
<motion.div
|
||||
animate={inView ? { scale: [1, 1.05, 1] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute top-4 left-4 w-20 h-32 bg-white/10 rounded-lg border border-purple-400/30 p-2"
|
||||
>
|
||||
<div className="w-full h-4 bg-purple-400/20 rounded mb-2"></div>
|
||||
<div className="space-y-1">
|
||||
<div className="w-full h-2 bg-purple-400/15 rounded"></div>
|
||||
<div className="w-3/4 h-2 bg-purple-400/10 rounded"></div>
|
||||
</div>
|
||||
<Smartphone className="absolute bottom-2 right-2 w-4 h-4 text-purple-400/50" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={inView ? { scale: [1, 1.03, 1] } : {}}
|
||||
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
|
||||
className="absolute top-4 right-4 w-24 h-16 bg-white/10 rounded-lg border border-pink-400/30 p-2"
|
||||
>
|
||||
<div className="w-full h-3 bg-pink-400/20 rounded mb-1"></div>
|
||||
<div className="w-2/3 h-2 bg-pink-400/15 rounded"></div>
|
||||
<Monitor className="absolute bottom-1 right-1 w-3 h-3 text-pink-400/50" />
|
||||
</motion.div>
|
||||
|
||||
{/* Design Tools */}
|
||||
<motion.div
|
||||
animate={inView ? { rotate: [0, 360] } : {}}
|
||||
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute bottom-8 left-8"
|
||||
>
|
||||
<Palette className="w-8 h-8 text-purple-400" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CodeEditorVisual = ({ inView }: { inView: boolean }) => {
|
||||
const [typedText, setTypedText] = useState("");
|
||||
const codeSnippet = "const app = () => {\n return <div>Hello World</div>\n}";
|
||||
|
||||
useEffect(() => {
|
||||
if (inView) {
|
||||
let i = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (i < codeSnippet.length) {
|
||||
setTypedText(codeSnippet.slice(0, i + 1));
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
setTypedText("");
|
||||
}
|
||||
}, [inView]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-green-500/10 to-emerald-500/10 p-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={inView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-gray-900/50 rounded-lg p-4 h-full"
|
||||
>
|
||||
{/* Code Editor Interface */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-3 h-3 bg-red-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-yellow-400 rounded-full"></div>
|
||||
<div className="w-3 h-3 bg-green-400 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* Typed Code */}
|
||||
<pre className="text-green-400 text-sm font-mono">
|
||||
{typedText}
|
||||
<motion.span
|
||||
animate={{ opacity: [1, 0, 1] }}
|
||||
transition={{ duration: 1, repeat: Infinity }}
|
||||
className="bg-green-400 w-2 h-4 inline-block ml-1"
|
||||
/>
|
||||
</pre>
|
||||
|
||||
{/* Tech Stack Icons */}
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<motion.div
|
||||
animate={inView ? { y: [-2, 2, -2] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<Database className="w-6 h-6 text-green-400" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={inView ? { y: [2, -2, 2] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
|
||||
>
|
||||
<Cloud className="w-6 h-6 text-emerald-400" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LaunchVisual = ({ inView }: { inView: boolean }) => {
|
||||
return (
|
||||
<div className="relative w-full h-64 overflow-hidden rounded-lg bg-gradient-to-br from-orange-500/10 to-red-500/10 p-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={inView ? { opacity: 1 } : { opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="relative h-full"
|
||||
>
|
||||
{/* Rocket Animation */}
|
||||
<motion.div
|
||||
animate={inView ? { y: [-20, -40, -20], x: [0, 10, 0] } : {}}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
||||
>
|
||||
<Rocket className="w-12 h-12 text-orange-400" />
|
||||
<motion.div
|
||||
animate={inView ? { scale: [0.8, 1.2, 0.8], opacity: [0.3, 0.7, 0.3] } : {}}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-8 bg-orange-400/30 rounded-full blur-sm"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Analytics Dashboard */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={inView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
className="absolute top-4 right-4 w-24 h-16 bg-white/10 rounded p-2"
|
||||
>
|
||||
<div className="flex items-end justify-between h-full">
|
||||
<motion.div
|
||||
animate={inView ? { height: ["40%", "60%", "40%"] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="w-2 bg-orange-400/60 rounded-t"
|
||||
/>
|
||||
<motion.div
|
||||
animate={inView ? { height: ["60%", "80%", "60%"] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.3 }}
|
||||
className="w-2 bg-red-400/60 rounded-t"
|
||||
/>
|
||||
<motion.div
|
||||
animate={inView ? { height: ["30%", "70%", "30%"] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 0.6 }}
|
||||
className="w-2 bg-yellow-400/60 rounded-t"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Success Indicators */}
|
||||
<motion.div
|
||||
animate={inView ? { scale: [0, 1, 0] } : {}}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut", delay: 1 }}
|
||||
className="absolute top-8 left-8"
|
||||
>
|
||||
<CheckCircle className="w-8 h-8 text-green-400" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProcessStep = ({ step, index, inView }: { step: typeof steps[0]; index: number; inView: boolean }) => {
|
||||
const Icon = step.icon;
|
||||
|
||||
const renderVisual = () => {
|
||||
switch (step.visual) {
|
||||
case "workspace_parallax":
|
||||
return <WorkspaceVisual inView={inView} />;
|
||||
case "figma_canvas_animation":
|
||||
return <FigmaCanvasVisual inView={inView} />;
|
||||
case "code_editor_animation":
|
||||
return <CodeEditorVisual inView={inView} />;
|
||||
case "launch_animation":
|
||||
return <LaunchVisual inView={inView} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isEven = index % 2 === 0;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: index * 0.2 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
className={`grid lg:grid-cols-2 gap-12 items-center ${!isEven ? "lg:flex-row-reverse" : ""}`}
|
||||
>
|
||||
{/* Content */}
|
||||
<div className={`space-y-6 ${!isEven ? "lg:order-2" : ""}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||
className="w-16 h-16 bg-accent/10 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Icon className="w-8 h-8 text-accent" />
|
||||
</motion.div>
|
||||
<div className="text-sm text-accent font-medium">
|
||||
Step {index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="text-3xl lg:text-4xl font-semibold text-foreground"
|
||||
>
|
||||
{step.title}
|
||||
</motion.h3>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="text-muted-foreground text-lg leading-relaxed"
|
||||
>
|
||||
{step.subtext}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Visual */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: isEven ? 50 : -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
className={`${!isEven ? "lg:order-1" : ""}`}
|
||||
>
|
||||
{renderVisual()}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ScrollParallaxProcess = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollYProgress } = useScroll({
|
||||
target: containerRef,
|
||||
offset: ["start end", "end start"]
|
||||
});
|
||||
|
||||
const springScrollProgress = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
});
|
||||
|
||||
const y1 = useTransform(springScrollProgress, [0, 1], [0, -100]);
|
||||
const y2 = useTransform(springScrollProgress, [0, 1], [0, -200]);
|
||||
const y3 = useTransform(springScrollProgress, [0, 1], [0, -50]);
|
||||
|
||||
return (
|
||||
<section ref={containerRef} className="relative py-20 overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
{/* Parallax Background Elements */}
|
||||
<motion.div
|
||||
style={{ y: y1 }}
|
||||
className="absolute top-20 left-10 w-32 h-32 bg-accent/5 rounded-full blur-3xl"
|
||||
/>
|
||||
<motion.div
|
||||
style={{ y: y2 }}
|
||||
className="absolute bottom-20 right-10 w-48 h-48 bg-blue-500/5 rounded-full blur-3xl"
|
||||
/>
|
||||
<motion.div
|
||||
style={{ y: y3 }}
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-purple-500/5 rounded-full blur-3xl"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-20"
|
||||
>
|
||||
<h2 className="text-4xl lg:text-5xl font-semibold text-foreground mb-6">
|
||||
How We Turn an Idea into a{" "}
|
||||
<span className="text-accent">Scalable Product</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-xl max-w-3xl mx-auto leading-relaxed">
|
||||
Our proven process transforms your vision into reality through strategic planning,
|
||||
thoughtful design, and expert engineering—every step of the way.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="space-y-32">
|
||||
{steps.map((step, index) => (
|
||||
<ProcessStep
|
||||
key={step.id}
|
||||
step={step}
|
||||
index={index}
|
||||
inView={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Final CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mt-20"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-accent hover:bg-accent/90 text-accent-foreground px-8 py-4 rounded-lg group"
|
||||
>
|
||||
Let's Build Yours
|
||||
<motion.div
|
||||
animate={{ x: [0, 5, 0] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</motion.div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
135
components/ServicesGrid.tsx
Normal file
135
components/ServicesGrid.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Smartphone, Globe, Palette, Brain, RefreshCw, Users } from "lucide-react";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "Mobile App Development",
|
||||
icon: Smartphone,
|
||||
description: "Native and cross-platform mobile solutions",
|
||||
href: "/services/mobile-app-development"
|
||||
},
|
||||
{
|
||||
title: "Web & SaaS Engineering",
|
||||
icon: Globe,
|
||||
description: "Scalable web applications and SaaS platforms"
|
||||
},
|
||||
{
|
||||
title: "UI/UX & Prototyping",
|
||||
icon: Palette,
|
||||
description: "User-centered design and interactive prototypes"
|
||||
},
|
||||
{
|
||||
title: "AI & Data Intelligence",
|
||||
icon: Brain,
|
||||
description: "Machine learning and data-driven solutions"
|
||||
},
|
||||
{
|
||||
title: "Product Modernization",
|
||||
icon: RefreshCw,
|
||||
description: "Legacy system upgrades and optimization"
|
||||
},
|
||||
{
|
||||
title: "Dedicated Development Teams",
|
||||
icon: Users,
|
||||
description: "Extended teams and staff augmentation"
|
||||
},
|
||||
];
|
||||
|
||||
const ServiceCard = ({ service, index }: { service: typeof services[0]; index: number }) => {
|
||||
const Icon = service.icon;
|
||||
|
||||
const handleClick = () => {
|
||||
if (service.href) {
|
||||
navigateTo(service.href);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{
|
||||
scale: 1.05,
|
||||
y: -8
|
||||
}}
|
||||
className={`group relative p-8 rounded-2xl bg-white/5 backdrop-blur-sm border border-white/10 hover:border-[#E5195E] transition-all duration-500 overflow-hidden ${
|
||||
service.href ? 'cursor-pointer' : 'cursor-default'
|
||||
}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<div className="mb-6">
|
||||
{/* Icon container with glassmorphism effect */}
|
||||
<div className="relative w-16 h-16 rounded-xl bg-gradient-to-br from-[#E5195E]/20 to-white/10 flex items-center justify-center mb-6 group-hover:scale-110 transition-all duration-500 backdrop-blur-sm border border-white/10">
|
||||
{/* Icon glow effect */}
|
||||
<div className="absolute inset-0 rounded-xl bg-[#E5195E]/20 blur-xl opacity-0 group-hover:opacity-60 transition-opacity duration-500" />
|
||||
<Icon className="relative w-8 h-8 text-[#E5195E] transition-colors duration-300" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-white mb-3 group-hover:text-white transition-colors duration-300">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-[#CCCCCC] text-sm leading-relaxed group-hover:text-white/90 transition-colors duration-300">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arrow indicator - only show for clickable services */}
|
||||
{service.href && (
|
||||
<div className="flex items-center justify-end opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-x-4 group-hover:translate-x-0">
|
||||
<div className="w-8 h-8 rounded-full bg-[#E5195E]/20 backdrop-blur-sm flex items-center justify-center border border-[#E5195E]/30">
|
||||
<svg
|
||||
className="w-4 h-4 text-[#E5195E]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hover overlay for clickable services */}
|
||||
{service.href && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#E5195E]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ServicesGrid = () => {
|
||||
return (
|
||||
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
|
||||
What We Do
|
||||
</h2>
|
||||
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
|
||||
We deliver comprehensive digital solutions that transform ideas into powerful, scalable applications
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<ServiceCard key={service.title} service={service} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
125
components/ServicesSection.tsx
Normal file
125
components/ServicesSection.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
export function ServicesSection() {
|
||||
const services = [
|
||||
{
|
||||
title: "Mobile App Development",
|
||||
description: "Native & cross-platform apps with pixel-perfect UIs and seamless user experiences.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-purple-500/20"
|
||||
},
|
||||
{
|
||||
title: "Web Platforms",
|
||||
description: "Scalable, secure, and SEO-optimized web applications built for performance.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-blue-500/20"
|
||||
},
|
||||
{
|
||||
title: "AI & ML Solutions",
|
||||
description: "Intelligent features powered by cutting-edge algorithms and machine learning.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-cyan-500/20"
|
||||
},
|
||||
{
|
||||
title: "DevOps & Cloud",
|
||||
description: "CI/CD pipelines and managed cloud infrastructure for seamless deployment.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-orange-500/20"
|
||||
},
|
||||
{
|
||||
title: "Product Design",
|
||||
description: "Human-centered UX with delightful micro-interactions and intuitive interfaces.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-pink-500/20"
|
||||
},
|
||||
{
|
||||
title: "Security & Compliance",
|
||||
description: "Pen-testing, auditing, and regulatory alignment for enterprise-grade security.",
|
||||
icon: (
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
),
|
||||
gradient: "from-[#E5195E]/20 to-green-500/20"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="services" className="border-t border-gray-800">
|
||||
<div className="container mx-auto px-6 lg:px-8 py-24">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold tracking-tight text-white">What We Do</h2>
|
||||
<p className="mt-4 text-gray-400 max-w-2xl mx-auto">
|
||||
End-to-end solutions for every stage of your product lifecycle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Services Grid - Wider container and larger cards */}
|
||||
<div className="mt-16 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-8xl mx-auto">
|
||||
{services.map((service, index) => (
|
||||
<div key={index} className="group relative p-10 border border-gray-800/50 hover:border-gray-700/70 transition-all duration-300 rounded-[10px] backdrop-blur-sm hover:backdrop-blur-md">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-900/20 via-gray-800/10 to-gray-900/20 rounded-[10px] opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
{/* Glassmorphism Icon */}
|
||||
<div className={`relative mb-8 w-20 h-20 rounded-2xl bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-xl border border-white/20 flex items-center justify-center group-hover:scale-110 transition-transform duration-300`}>
|
||||
<div className={`absolute inset-0 bg-gradient-to-br ${service.gradient} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300`}></div>
|
||||
<svg
|
||||
className="w-10 h-10 text-white/90 relative z-10"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
{service.icon}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-white tracking-tight mb-4">
|
||||
<span className="text-[#E5195E]">{service.title.split(' ')[0]}</span>
|
||||
{service.title.split(' ').slice(1).join(' ') && ` ${service.title.split(' ').slice(1).join(' ')}`}
|
||||
</h3>
|
||||
<p className="text-gray-400 leading-relaxed">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
99
components/SplineFallback.tsx
Normal file
99
components/SplineFallback.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
export function SplineFallback() {
|
||||
return (
|
||||
<div className="w-full h-full bg-gradient-to-br from-gray-900/90 to-gray-800/90 backdrop-blur-sm rounded-xl flex items-center justify-center overflow-hidden relative">
|
||||
{/* Animated background gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/10 to-purple-500/10 animate-pulse"></div>
|
||||
|
||||
{/* Grid pattern overlay */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<svg className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="white" strokeWidth="1"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Main 3D scene */}
|
||||
<div className="relative z-10 w-full h-full flex items-center justify-center">
|
||||
{/* Central 3D composition */}
|
||||
<div className="relative">
|
||||
{/* Main floating cube with nested elements */}
|
||||
<div className="w-40 h-40 relative transform-gpu">
|
||||
{/* Outer cube */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#E5195E]/30 to-purple-500/30 rounded-3xl transform rotate-12 animate-[float_4s_ease-in-out_infinite] backdrop-blur-sm border border-white/20 shadow-2xl">
|
||||
{/* Middle cube */}
|
||||
<div className="absolute inset-6 bg-gradient-to-br from-blue-500/40 to-cyan-500/40 rounded-2xl transform -rotate-12 animate-[float_3s_ease-in-out_infinite_0.5s] backdrop-blur-sm border border-white/10">
|
||||
{/* Inner cube */}
|
||||
<div className="absolute inset-6 bg-gradient-to-br from-green-500/50 to-emerald-500/50 rounded-xl transform rotate-6 animate-[float_2s_ease-in-out_infinite_1s] backdrop-blur-sm border border-white/5">
|
||||
{/* Core element */}
|
||||
<div className="absolute inset-4 bg-gradient-to-br from-orange-500/60 to-yellow-500/60 rounded-lg animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orbiting satellites */}
|
||||
<div className="absolute top-0 left-0 w-full h-full">
|
||||
{/* Satellite 1 */}
|
||||
<div className="absolute -top-8 -right-8 w-8 h-8 bg-gradient-to-r from-[#E5195E] to-pink-500 rounded-xl transform rotate-45 animate-[orbit_8s_linear_infinite] shadow-lg">
|
||||
<div className="absolute inset-1 bg-white/20 rounded-lg animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
{/* Satellite 2 */}
|
||||
<div className="absolute -bottom-6 -left-8 w-6 h-6 bg-gradient-to-r from-blue-500 to-cyan-400 rounded-lg transform -rotate-12 animate-[orbit_6s_linear_infinite_reverse] shadow-lg">
|
||||
<div className="absolute inset-1 bg-white/30 rounded-sm animate-pulse delay-500"></div>
|
||||
</div>
|
||||
|
||||
{/* Satellite 3 */}
|
||||
<div className="absolute top-1/2 -left-16 w-5 h-5 bg-gradient-to-r from-green-400 to-emerald-400 rounded-full animate-[orbit_10s_linear_infinite] shadow-lg">
|
||||
<div className="absolute inset-1 bg-white/40 rounded-full animate-pulse delay-1000"></div>
|
||||
</div>
|
||||
|
||||
{/* Satellite 4 */}
|
||||
<div className="absolute top-1/4 -right-14 w-7 h-7 bg-gradient-to-r from-purple-500 to-violet-400 rounded-2xl transform rotate-30 animate-[orbit_7s_linear_infinite_reverse] shadow-lg">
|
||||
<div className="absolute inset-1.5 bg-white/25 rounded-xl animate-pulse delay-1500"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating particles */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{/* Particle 1 */}
|
||||
<div className="absolute top-1/6 left-1/5 w-2 h-2 bg-white/60 rounded-full animate-[float_3s_ease-in-out_infinite] shadow-sm"></div>
|
||||
|
||||
{/* Particle 2 */}
|
||||
<div className="absolute top-2/3 left-4/5 w-1.5 h-1.5 bg-[#E5195E]/80 rounded-full animate-[float_4s_ease-in-out_infinite_0.8s] shadow-sm"></div>
|
||||
|
||||
{/* Particle 3 */}
|
||||
<div className="absolute top-1/2 left-1/8 w-1 h-1 bg-blue-400/70 rounded-full animate-[float_2.5s_ease-in-out_infinite_1.2s] shadow-sm"></div>
|
||||
|
||||
{/* Particle 4 */}
|
||||
<div className="absolute top-1/4 right-1/6 w-1.5 h-1.5 bg-green-400/60 rounded-full animate-[float_3.5s_ease-in-out_infinite_1.8s] shadow-sm"></div>
|
||||
|
||||
{/* Particle 5 */}
|
||||
<div className="absolute bottom-1/4 left-1/3 w-1 h-1 bg-purple-400/50 rounded-full animate-[float_4.5s_ease-in-out_infinite_2.2s] shadow-sm"></div>
|
||||
</div>
|
||||
|
||||
{/* Light rays */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 w-1 h-32 bg-gradient-to-t from-transparent via-white/10 to-transparent transform -translate-x-1/2 -translate-y-1/2 rotate-45 animate-[rotate_20s_linear_infinite]"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-1 h-24 bg-gradient-to-t from-transparent via-[#E5195E]/10 to-transparent transform -translate-x-1/2 -translate-y-1/2 rotate-135 animate-[rotate_15s_linear_infinite_reverse]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
<div className="absolute bottom-6 left-6 flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-xs text-white/70 font-medium">Interactive 3D Experience</span>
|
||||
</div>
|
||||
|
||||
{/* Tech badge */}
|
||||
<div className="absolute top-6 right-6 px-3 py-1 bg-black/40 backdrop-blur-sm rounded-full border border-white/10">
|
||||
<span className="text-xs text-white/80 font-medium">AI Powered</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
components/SplineViewer.tsx
Normal file
7
components/SplineViewer.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is no longer needed as we're using the official Spline component
|
||||
// from '@splinetool/react-spline/next' directly in the HeroSection component.
|
||||
// Keeping this file empty to avoid any import errors until references are cleaned up.
|
||||
|
||||
export function SplineViewer() {
|
||||
return null;
|
||||
}
|
||||
118
components/SplitCallToAction.tsx
Normal file
118
components/SplitCallToAction.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Phone, Clock, Zap, Calendar, MessageSquare } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
import { navigateTo } from "../App";
|
||||
|
||||
export const SplitCallToAction = () => {
|
||||
return (
|
||||
<section className="relative py-20 overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center max-w-6xl mx-auto">
|
||||
{/* Left Content */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl lg:text-5xl font-semibold text-foreground mb-6 whitespace-nowrap">
|
||||
Ready to Build with WDI?
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground leading-relaxed mb-8">
|
||||
Schedule a no-commitment discovery call with our consulting team. Let's discuss your vision and create a roadmap to success.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
|
||||
<MessageSquare className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-foreground font-medium">Free Consultation</div>
|
||||
<div className="text-muted-foreground text-sm">No sales pitch, just honest advice</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
|
||||
<Clock className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-foreground font-medium">Flexible Scheduling</div>
|
||||
<div className="text-muted-foreground text-sm">Available across all time zones</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white/10 backdrop-blur-sm rounded-lg border border-white/20 flex items-center justify-center">
|
||||
<Zap className="w-6 h-6 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-foreground font-medium">Quick Response</div>
|
||||
<div className="text-muted-foreground text-sm">We'll get back to you within 2 hours</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right CTA */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 50 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative"
|
||||
>
|
||||
<div className="bg-card/50 backdrop-blur-sm rounded-lg p-8 border border-border">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-20 h-20 mx-auto mb-6 bg-white/10 backdrop-blur-sm rounded-full border border-white/20 flex items-center justify-center">
|
||||
<Calendar className="w-10 h-10 text-accent" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-semibold text-foreground mb-2">
|
||||
Book a Discovery Call
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
Let's discuss your project and explore how we can help you succeed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-accent hover:bg-accent/90 text-accent-foreground py-4 text-lg border-0 rounded-lg"
|
||||
onClick={() => navigateTo('/contact')}
|
||||
>
|
||||
<Phone className="w-5 h-5 mr-2" />
|
||||
Schedule Free Call
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-border">
|
||||
<div className="grid grid-cols-3 gap-4 text-center text-sm">
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">200+</div>
|
||||
<div className="text-muted-foreground">Projects</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">25+</div>
|
||||
<div className="text-muted-foreground">Years</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-foreground">15+</div>
|
||||
<div className="text-muted-foreground">Countries</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
94
components/StepsIllustrated.tsx
Normal file
94
components/StepsIllustrated.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { FileText, Palette, Code, Rocket } from "lucide-react";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "Define Scope",
|
||||
description: "We analyze your requirements and create a detailed project roadmap with clear timelines.",
|
||||
icon: FileText,
|
||||
color: "from-blue-500 to-cyan-500"
|
||||
},
|
||||
{
|
||||
title: "Design UI/UX",
|
||||
description: "Our designers create intuitive, user-centered interfaces that align with your brand.",
|
||||
icon: Palette,
|
||||
color: "from-purple-500 to-pink-500"
|
||||
},
|
||||
{
|
||||
title: "Develop with Agile Sprints",
|
||||
description: "We build your product using agile methodology with regular updates and feedback loops.",
|
||||
icon: Code,
|
||||
color: "from-green-500 to-emerald-500"
|
||||
},
|
||||
{
|
||||
title: "Test, Launch & Scale",
|
||||
description: "Comprehensive testing, smooth deployment, and ongoing support for continuous growth.",
|
||||
icon: Rocket,
|
||||
color: "from-[#E5195E] to-orange-500"
|
||||
}
|
||||
];
|
||||
|
||||
const StepCard = ({ step, index }: { step: typeof steps[0]; index: number }) => {
|
||||
const Icon = step.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
className="relative group"
|
||||
>
|
||||
{/* Connection Line */}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="hidden lg:block absolute top-16 left-full w-full h-0.5 bg-gradient-to-r from-white/20 to-transparent z-0" />
|
||||
)}
|
||||
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="mb-6">
|
||||
<div className={`w-20 h-20 mx-auto rounded-2xl bg-gradient-to-br ${step.color} flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300`}>
|
||||
<Icon className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<div className="w-8 h-8 mx-auto rounded-full bg-[#E5195E] flex items-center justify-center text-white font-bold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-white mb-4">{step.title}</h3>
|
||||
<p className="text-[#CCCCCC] leading-relaxed max-w-sm mx-auto">{step.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StepsIllustrated = () => {
|
||||
return (
|
||||
<section className="relative py-20 bg-[#121212] overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
|
||||
How We Turn Your Idea Into a Scalable Product
|
||||
</h2>
|
||||
<p className="text-[#CCCCCC] text-lg max-w-2xl mx-auto">
|
||||
Our proven 4-step process ensures your project is delivered on time, on budget, and exceeds expectations.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid lg:grid-cols-4 gap-12 max-w-6xl mx-auto">
|
||||
{steps.map((step, index) => (
|
||||
<StepCard key={step.title} step={step} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
72
components/WhyChooseWDI.tsx
Normal file
72
components/WhyChooseWDI.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Wrench, Shield, Zap } from "lucide-react";
|
||||
import { GridPattern } from "./GridPattern";
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Wrench,
|
||||
title: "24+ Years of Product Engineering",
|
||||
description: "Deep expertise in building scalable, production-ready solutions"
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "100% Project Ownership & IP Transfer",
|
||||
description: "Complete intellectual property rights and full project ownership"
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Agile, Transparent, and Outcome-Driven",
|
||||
description: "Fast delivery with clear communication and measurable results"
|
||||
},
|
||||
];
|
||||
|
||||
const FeatureCard = ({ feature, index }: { feature: typeof features[0]; index: number }) => {
|
||||
const Icon = feature.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: index * 0.2 }}
|
||||
viewport={{ once: true }}
|
||||
whileHover={{ y: -5 }}
|
||||
className="text-center group"
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="w-20 h-20 mx-auto rounded-2xl bg-[#E5195E]/10 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<Icon className="w-10 h-10 text-[#E5195E]" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-4">{feature.title}</h3>
|
||||
<p className="text-[#CCCCCC] leading-relaxed max-w-sm mx-auto">{feature.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WhyChooseWDI = () => {
|
||||
return (
|
||||
<section className="relative py-20 bg-[#0E0E0E] overflow-hidden">
|
||||
<GridPattern strokeDasharray="4 2" />
|
||||
|
||||
<div className="relative z-10 container mx-auto px-6 lg:px-8">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
viewport={{ once: true }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl lg:text-4xl font-semibold text-white mb-4">
|
||||
Why Leading Startups Choose WDI
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-12 max-w-6xl mx-auto">
|
||||
{features.map((feature, index) => (
|
||||
<FeatureCard key={feature.title} feature={feature} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
27
components/figma/ImageWithFallback.tsx
Normal file
27
components/figma/ImageWithFallback.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const ERROR_IMG_SRC =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
|
||||
|
||||
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||
const [didError, setDidError] = useState(false)
|
||||
|
||||
const handleError = () => {
|
||||
setDidError(true)
|
||||
}
|
||||
|
||||
const { src, alt, style, className, ...rest } = props
|
||||
|
||||
return didError ? (
|
||||
<div
|
||||
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
|
||||
)
|
||||
}
|
||||
55
components/ui/accordion.tsx
Normal file
55
components/ui/accordion.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "./utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
157
components/ui/alert-dialog.tsx
Normal file
157
components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
66
components/ui/alert.tsx
Normal file
66
components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
11
components/ui/aspect-ratio.tsx
Normal file
11
components/ui/aspect-ratio.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
53
components/ui/avatar.tsx
Normal file
53
components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
46
components/ui/badge.tsx
Normal file
46
components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
41
components/ui/border-beam.tsx
Normal file
41
components/ui/border-beam.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { cn } from "./utils";
|
||||
|
||||
interface BorderBeamProps {
|
||||
className?: string;
|
||||
size?: number;
|
||||
duration?: number;
|
||||
borderWidth?: number;
|
||||
colorFrom?: string;
|
||||
colorTo?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const BorderBeam = ({
|
||||
className,
|
||||
size = 200,
|
||||
duration = 8,
|
||||
borderWidth = 2,
|
||||
colorFrom = "#E5195E",
|
||||
colorTo = "#ffffff",
|
||||
delay = 0,
|
||||
}: BorderBeamProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-[inherit]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 rounded-[inherit] opacity-75"
|
||||
style={{
|
||||
background: `conic-gradient(from 0deg, transparent, ${colorFrom}, ${colorTo}, transparent)`,
|
||||
WebkitMask: `radial-gradient(farthest-side at center, transparent calc(100% - ${borderWidth}px), white calc(100% - ${borderWidth}px))`,
|
||||
mask: `radial-gradient(farthest-side at center, transparent calc(100% - ${borderWidth}px), white calc(100% - ${borderWidth}px))`,
|
||||
animation: `border-beam ${duration}s linear infinite`,
|
||||
animationDelay: `${delay}s`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
109
components/ui/breadcrumb.tsx
Normal file
109
components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "./utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"btn-elevation inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "btn-primary-wdi",
|
||||
destructive: "btn-destructive-wdi",
|
||||
outline: "btn-outline-wdi",
|
||||
secondary: "btn-secondary-wdi",
|
||||
ghost: "btn-ghost-wdi",
|
||||
link: "btn-link-wdi",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2 text-sm",
|
||||
sm: "btn-sm h-8 rounded-md px-3 text-xs",
|
||||
lg: "btn-lg h-11 rounded-md px-8",
|
||||
xl: "btn-xl h-12 rounded-md px-10",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, children, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
75
components/ui/calendar.tsx
Normal file
75
components/ui/calendar.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md",
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100",
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<h4
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6 [&:last-child]:pb-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
241
components/ui/carousel.tsx
Normal file
241
components/ui/carousel.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import useEmblaCarousel, {
|
||||
type UseEmblaCarouselType,
|
||||
} from "embla-carousel-react";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { Button } from "./button";
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||
type CarouselOptions = UseCarouselParameters[0];
|
||||
type CarouselPlugin = UseCarouselParameters[1];
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions;
|
||||
plugins?: CarouselPlugin;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: boolean;
|
||||
canScrollNext: boolean;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
orientation = "horizontal",
|
||||
opts,
|
||||
setApi,
|
||||
plugins,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{
|
||||
...opts,
|
||||
axis: orientation === "horizontal" ? "x" : "y",
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||
|
||||
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||
if (!api) return;
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
}, []);
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev();
|
||||
}, [api]);
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext();
|
||||
}, [api]);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
},
|
||||
[scrollPrev, scrollNext],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api || !setApi) return;
|
||||
setApi(api);
|
||||
}, [api, setApi]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api) return;
|
||||
onSelect(api);
|
||||
api.on("reInit", onSelect);
|
||||
api.on("select", onSelect);
|
||||
|
||||
return () => {
|
||||
api?.off("select", onSelect);
|
||||
};
|
||||
}, [api, onSelect]);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api: api,
|
||||
opts,
|
||||
orientation:
|
||||
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
className={cn("relative", className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
data-slot="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="overflow-hidden"
|
||||
data-slot="carousel-content"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
data-slot="carousel-item"
|
||||
className={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselPrevious({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-previous"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -left-12 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={scrollPrev}
|
||||
{...props}
|
||||
>
|
||||
<ArrowLeft />
|
||||
<span className="sr-only">Previous slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function CarouselNext({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "icon",
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-slot="carousel-next"
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(
|
||||
"absolute size-8 rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "top-1/2 -right-12 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
className,
|
||||
)}
|
||||
disabled={!canScrollNext}
|
||||
onClick={scrollNext}
|
||||
{...props}
|
||||
>
|
||||
<ArrowRight />
|
||||
<span className="sr-only">Next slide</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
type CarouselApi,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselPrevious,
|
||||
CarouselNext,
|
||||
};
|
||||
353
components/ui/chart.tsx
Normal file
353
components/ui/chart.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
32
components/ui/checkbox.tsx
Normal file
32
components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
33
components/ui/collapsible.tsx
Normal file
33
components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
177
components/ui/command.tsx
Normal file
177
components/ui/command.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
252
components/ui/context-menu.tsx
Normal file
252
components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ContextMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
135
components/ui/dialog.tsx
Normal file
135
components/ui/dialog.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
132
components/ui/drawer.tsx
Normal file
132
components/ui/drawer.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
257
components/ui/dropdown-menu.tsx
Normal file
257
components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
168
components/ui/form.tsx
Normal file
168
components/ui/form.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { Label } from "./label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState } = useFormContext();
|
||||
const formState = useFormState({ name: fieldContext.name });
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message ?? "") : props.children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
44
components/ui/hover-card.tsx
Normal file
44
components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
<HoverCardPrimitive.Content
|
||||
data-slot="hover-card-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
77
components/ui/input-otp.tsx
Normal file
77
components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { MinusIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number;
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
21
components/ui/input.tsx
Normal file
21
components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
276
components/ui/menubar.tsx
Normal file
276
components/ui/menubar.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Menubar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||
return (
|
||||
<MenubarPrimitive.Root
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||
return (
|
||||
<MenubarPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||
return (
|
||||
<MenubarPortal>
|
||||
<MenubarPrimitive.Content
|
||||
data-slot="menubar-content"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||
return (
|
||||
<MenubarPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.Label
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||
return (
|
||||
<MenubarPrimitive.SubContent
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
};
|
||||
168
components/ui/navigation-menu.tsx
Normal file
168
components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
127
components/ui/pagination.tsx
Normal file
127
components/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { Button, buttonVariants } from "./button";
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />;
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">;
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
||||
48
components/ui/popover.tsx
Normal file
48
components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
31
components/ui/progress.tsx
Normal file
31
components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
45
components/ui/radio-group.tsx
Normal file
45
components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
56
components/ui/resizable.tsx
Normal file
56
components/ui/resizable.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
import * as ResizablePrimitive from "react-resizable-panels";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
58
components/ui/scroll-area.tsx
Normal file
58
components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
189
components/ui/select.tsx
Normal file
189
components/ui/select.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
27
components/ui/separator.tsx
Normal file
27
components/ui/separator.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
139
components/ui/sheet.tsx
Normal file
139
components/ui/sheet.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
29
components/ui/shimmer-button.tsx
Normal file
29
components/ui/shimmer-button.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { cn } from "./utils";
|
||||
|
||||
interface ShimmerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative inline-flex items-center justify-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-medium text-accent-foreground transition-all duration-300 hover:bg-accent/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent whitespace-nowrap overflow-hidden",
|
||||
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ShimmerButton.displayName = "ShimmerButton";
|
||||
|
||||
export { ShimmerButton };
|
||||
726
components/ui/sidebar.tsx
Normal file
726
components/ui/sidebar.tsx
Normal file
@@ -0,0 +1,726 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "./use-mobile";
|
||||
import { cn } from "./utils";
|
||||
import { Button } from "./button";
|
||||
import { Input } from "./input";
|
||||
import { Separator } from "./separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "./sheet";
|
||||
import { Skeleton } from "./skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
||||
}, [isMobile, setOpen, setOpenMobile]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
13
components/ui/skeleton.tsx
Normal file
13
components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
63
components/ui/slider.tsx
Normal file
63
components/ui/slider.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max],
|
||||
);
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={cn(
|
||||
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={cn(
|
||||
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Slider };
|
||||
25
components/ui/sonner.tsx
Normal file
25
components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
31
components/ui/switch.tsx
Normal file
31
components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-card dark:data-[state=unchecked]:bg-card-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch };
|
||||
116
components/ui/table.tsx
Normal file
116
components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
66
components/ui/tabs.tsx
Normal file
66
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
73
components/ui/toggle-group.tsx
Normal file
73
components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { toggleVariants } from "./toggle";
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
47
components/ui/toggle.tsx
Normal file
47
components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
61
components/ui/tooltip.tsx
Normal file
61
components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
21
components/ui/use-mobile.ts
Normal file
21
components/ui/use-mobile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
6
components/ui/utils.ts
Normal file
6
components/ui/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
85
fix-figma-imports.md
Normal file
85
fix-figma-imports.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Fix Figma Asset Imports for Local Development
|
||||
|
||||
## Quick Fix Commands
|
||||
|
||||
Run these commands to install missing dependencies and fix the import issues:
|
||||
|
||||
```bash
|
||||
# 1. Install missing dependencies
|
||||
npm install @radix-ui/react-slot@^1.0.2 @tailwindcss/postcss@^4.0.0-alpha.31
|
||||
|
||||
# 2. Replace figma:asset imports with placeholder images
|
||||
# This script will replace all figma:asset imports with placeholder URLs
|
||||
|
||||
# For PowerShell (Windows):
|
||||
(Get-Content components/CarouselTestimonials.tsx) -replace 'import clutchLogo from "figma:asset/[^"]+";', '// import clutchLogo from "figma:asset/..."; // Placeholder image' | Set-Content components/CarouselTestimonials.tsx
|
||||
|
||||
# For each file with figma:asset imports, replace with a placeholder:
|
||||
# Example for CarouselTestimonials.tsx:
|
||||
```
|
||||
|
||||
## Manual Fix Instructions
|
||||
|
||||
For each file that has `figma:asset` imports, follow these steps:
|
||||
|
||||
### 1. Components/CarouselTestimonials.tsx
|
||||
Replace:
|
||||
```tsx
|
||||
import clutchLogo from "figma:asset/2e527c2f1a28e8f4cafbb9fd9f8f9d410530d352.png";
|
||||
```
|
||||
|
||||
With:
|
||||
```tsx
|
||||
// Placeholder for Clutch logo - replace with actual logo file
|
||||
const clutchLogo = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### 2. Other Files with figma:asset imports:
|
||||
- `pages/RegroupProject.tsx`
|
||||
- `pages/CaseStudies.tsx`
|
||||
- `pages/iOSAppDevelopment.tsx`
|
||||
- `pages/TanamiProject.tsx`
|
||||
- `components/CaseStudyHighlight.tsx`
|
||||
- `pages/SeezunProject.tsx`
|
||||
- `pages/WokaProject.tsx`
|
||||
- `pages/RanOutOfProject.tsx`
|
||||
- `imports/Group1597880681.tsx`
|
||||
|
||||
For each file, replace the `figma:asset` imports with appropriate placeholder images from Unsplash:
|
||||
|
||||
```tsx
|
||||
// Instead of: import imageName from "figma:asset/...";
|
||||
// Use: const imageName = "https://images.unsplash.com/photo-[relevant-photo-id]?w=400&h=300&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### 3. Recommended Placeholder Images:
|
||||
|
||||
**For project screenshots:**
|
||||
```tsx
|
||||
const projectImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
**For mobile app screenshots:**
|
||||
```tsx
|
||||
const mobileAppImage = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=300&h=600&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
**For logos:**
|
||||
```tsx
|
||||
const logoImage = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
**For tech/software images:**
|
||||
```tsx
|
||||
const techImage = "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=400&h=300&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
## After Making Changes
|
||||
|
||||
1. Run `npm install` to ensure all dependencies are installed
|
||||
2. Run `npm run dev` to start the development server
|
||||
3. The project should now run without the figma:asset import errors
|
||||
|
||||
## Note
|
||||
|
||||
These placeholder images are for development purposes only. In production, you should replace them with your actual project images, logos, and assets.
|
||||
122
fix-remaining-imports.md
Normal file
122
fix-remaining-imports.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Final Fix for Any Remaining Figma Imports
|
||||
|
||||
If you encounter any remaining `figma:asset` imports, here are the replacement patterns:
|
||||
|
||||
## Quick Fix Script (Run in project root)
|
||||
|
||||
### For Windows PowerShell:
|
||||
```powershell
|
||||
# Fix any remaining figma:asset imports in all TypeScript files
|
||||
Get-ChildItem -Recurse -Include "*.tsx","*.ts" | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
if ($content -match 'import.*from "figma:asset/[^"]+";') {
|
||||
Write-Host "Fixing: $($_.Name)"
|
||||
|
||||
# Replace figma:asset imports with placeholder images
|
||||
$content = $content -replace 'import\s+(\w+)\s+from\s+"figma:asset/[^"]+";', 'const $1 = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";'
|
||||
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### For Mac/Linux Bash:
|
||||
```bash
|
||||
# Find and fix all figma:asset imports
|
||||
find . -name "*.tsx" -o -name "*.ts" | grep -v node_modules | xargs grep -l "figma:asset" | while read file; do
|
||||
echo "Fixing: $file"
|
||||
sed -i 's/import \([a-zA-Z0-9_]*\) from "figma:asset\/[^"]*";/const \1 = "https:\/\/images.unsplash.com\/photo-1551650975-87deedd944c3?w=600\&h=400\&fit=crop\&auto=format";/g' "$file"
|
||||
done
|
||||
```
|
||||
|
||||
## Manual Replacement Patterns
|
||||
|
||||
If you prefer to fix them manually, replace any remaining:
|
||||
|
||||
### Project Images:
|
||||
```tsx
|
||||
// OLD:
|
||||
import projectImage from "figma:asset/...";
|
||||
|
||||
// NEW:
|
||||
const projectImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### Logo Images:
|
||||
```tsx
|
||||
// OLD:
|
||||
import logoImage from "figma:asset/...";
|
||||
|
||||
// NEW:
|
||||
const logoImage = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### Profile Images:
|
||||
```tsx
|
||||
// OLD:
|
||||
import profileImage from "figma:asset/...";
|
||||
|
||||
// NEW:
|
||||
const profileImage = "https://images.unsplash.com/photo-1494790108755-2616b332c5cd?w=150&h=150&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### Mobile App Screenshots:
|
||||
```tsx
|
||||
// OLD:
|
||||
import appScreenshot from "figma:asset/...";
|
||||
|
||||
// NEW:
|
||||
const appScreenshot = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=300&h=600&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
## Image Categories by Use Case
|
||||
|
||||
### Technology/Development:
|
||||
- `https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=600&h=400&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format`
|
||||
|
||||
### Business/Corporate:
|
||||
- `https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=600&h=400&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1542744173-8e7e53415bb0?w=600&h=400&fit=crop&auto=format`
|
||||
|
||||
### Mobile Apps:
|
||||
- `https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=300&h=600&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&auto=format`
|
||||
|
||||
### Healthcare:
|
||||
- `https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=600&h=400&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1559757148-5c350d0d3c56?w=400&h=300&fit=crop&auto=format`
|
||||
|
||||
### Finance/FinTech:
|
||||
- `https://images.unsplash.com/photo-1559526324-593bc073d938?w=600&h=400&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=600&h=400&fit=crop&auto=format`
|
||||
|
||||
### E-commerce:
|
||||
- `https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=600&h=400&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=600&h=400&fit=crop&auto=format`
|
||||
|
||||
### Professional Headshots:
|
||||
- `https://images.unsplash.com/photo-1494790108755-2616b332c5cd?w=150&h=150&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&auto=format`
|
||||
- `https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&h=150&fit=crop&auto=format`
|
||||
|
||||
## Verification
|
||||
|
||||
After running the fix:
|
||||
|
||||
1. **Search for remaining imports**:
|
||||
```bash
|
||||
grep -r "figma:asset" . --exclude-dir=node_modules
|
||||
```
|
||||
|
||||
2. **Start the dev server**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **Check for errors**:
|
||||
- No import resolution errors
|
||||
- All images load properly
|
||||
- Clean console output
|
||||
|
||||
The project should now run completely error-free! 🎉
|
||||
94
imports/BlackLogo14.tsx
Normal file
94
imports/BlackLogo14.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import svgPaths from "./svg-wbq66h6oh8";
|
||||
|
||||
export default function BlackLogo14() {
|
||||
return (
|
||||
<div className="relative size-full" data-name="black_logo (1) 4">
|
||||
<svg
|
||||
className="block size-full"
|
||||
fill="none"
|
||||
preserveAspectRatio="none"
|
||||
viewBox="0 0 76 76"
|
||||
>
|
||||
<g clipPath="url(#clip0_20_215)" id="black_logo (1) 4">
|
||||
<path
|
||||
d={svgPaths.p2e932e00}
|
||||
fill="var(--fill-0, #E8155D)"
|
||||
id="Vector"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.pd29a8f2}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_2"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p2dde0400}
|
||||
fill="var(--fill-0, #E8155D)"
|
||||
id="Vector_3"
|
||||
/>
|
||||
<g id="Group">
|
||||
<path
|
||||
d={svgPaths.p280e9a00}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_4"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p160b7e80}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_5"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p12200e80}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_6"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p3f37ee80}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_7"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p3cb8f80}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_8"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p63abc40}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_9"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p23e6b300}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_10"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p3d04c980}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_11"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p340aab00}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_12"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p2bb50bf0}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_13"
|
||||
/>
|
||||
<path
|
||||
d={svgPaths.p33eaf500}
|
||||
fill="var(--fill-0, white)"
|
||||
id="Vector_14"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_20_215">
|
||||
<rect fill="white" height="76" width="76" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
imports/Group1597880681.tsx
Normal file
44
imports/Group1597880681.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
|
||||
// High-quality placeholder images for the group icons
|
||||
const logoImage1 = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage2 = "https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage3 = "https://images.unsplash.com/photo-1542744173-8e7e53415bb0?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage4 = "https://images.unsplash.com/photo-1560472355-536de3962603?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage5 = "https://images.unsplash.com/photo-1563986768609-322da13575f3?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage6 = "https://images.unsplash.com/photo-1572177812156-58036aae439c?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage7 = "https://images.unsplash.com/photo-1553484771-371a605b060b?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage8 = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
const logoImage9 = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=120&h=60&fit=crop&auto=format";
|
||||
|
||||
export default function Group1597880681() {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-8 flex-wrap opacity-60 hover:opacity-100 transition-opacity duration-300">
|
||||
{/* Client Logos Grid */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-9 gap-6 items-center">
|
||||
{[
|
||||
{ src: logoImage1, alt: "Client Logo 1" },
|
||||
{ src: logoImage2, alt: "Client Logo 2" },
|
||||
{ src: logoImage3, alt: "Client Logo 3" },
|
||||
{ src: logoImage4, alt: "Client Logo 4" },
|
||||
{ src: logoImage5, alt: "Client Logo 5" },
|
||||
{ src: logoImage6, alt: "Client Logo 6" },
|
||||
{ src: logoImage7, alt: "Client Logo 7" },
|
||||
{ src: logoImage8, alt: "Client Logo 8" },
|
||||
{ src: logoImage9, alt: "Client Logo 9" }
|
||||
].map((logo, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-24 h-12 flex items-center justify-center p-2 rounded-lg bg-white/5 backdrop-blur-sm border border-white/10 hover:border-accent/30 transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.alt}
|
||||
className="max-w-full max-h-full object-contain filter brightness-0 invert opacity-70 hover:opacity-100 transition-opacity duration-300"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3719
imports/Group1597880906.tsx
Normal file
3719
imports/Group1597880906.tsx
Normal file
File diff suppressed because it is too large
Load Diff
68
imports/svg-nhiva44k5m.ts
Normal file
68
imports/svg-nhiva44k5m.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export default {
|
||||
p12311d00: "M15.1127 77.8435C13.6633 81.8918 15.0054 84.9968 15.0054 84.9968C15.0054 84.9968 18.0135 83.4501 19.463 79.4013C20.9125 75.3529 20.255 70.3359 20.255 70.3359C20.255 70.3359 16.5627 73.7947 15.1127 77.8435Z",
|
||||
p1317aa80: "M19.2514 87.2945C18.5269 91.5333 20.388 94.3579 20.388 94.3579C20.388 94.3579 23.0814 92.3119 23.8064 88.0731C24.5309 83.8344 23.0121 79.0078 23.0121 79.0078C23.0121 79.0078 19.9764 83.0557 19.2514 87.2945Z",
|
||||
p13583140: "M8.6526 72.1457C11.9324 74.9272 12.3224 78.2869 12.3224 78.2869C12.3224 78.2869 8.94397 78.4511 5.66417 75.6696C2.38437 72.8881 0.445312 68.2148 0.445312 68.2148C0.445312 68.2148 5.37281 69.3642 8.6526 72.1457Z",
|
||||
p13cd6e00: "M50.5019 121.38C54.5502 119.931 57.6552 121.273 57.6552 121.273C57.6552 121.273 56.108 124.281 52.0596 125.731C48.0113 127.18 42.9943 126.523 42.9943 126.523C42.9943 126.523 46.4535 122.83 50.5019 121.38Z",
|
||||
p13e1a230: "M41.3529 4.57602C38.871 6.1604 36.5938 5.73689 36.5938 5.73689C36.5938 5.73689 37.1687 3.49316 39.6505 1.90924C42.1329 0.324859 45.582 0 45.582 0C45.582 0 43.8352 2.99164 41.3529 4.57602Z",
|
||||
p14389100: "M21.6546 17.3975C21.6349 21.1912 19.5852 23.3602 19.5852 23.3602C19.5852 23.3602 17.5585 21.1701 17.5783 17.376C17.598 13.5823 19.6573 9.62109 19.6573 9.62109C19.6573 9.62109 21.6743 13.6038 21.6546 17.3975Z",
|
||||
p14eca800: "M27.2881 17.9702C23.5555 18.6484 21.7752 21.0435 21.7752 21.0435C21.7752 21.0435 24.2841 22.6591 28.0168 21.9809C31.7494 21.3028 35.2926 18.5869 35.2926 18.5869C35.2926 18.5869 31.0208 17.2916 27.2881 17.9702Z",
|
||||
p15f33f00: "M32.3629 111.367C36.6631 111.39 39.1211 113.713 39.1211 113.713C39.1211 113.713 36.6388 116.01 32.3385 115.988C28.0383 115.965 23.549 113.075 23.549 113.075C23.549 113.075 28.0626 111.345 32.3629 111.367Z",
|
||||
p17983c80: "M13.7241 45.7043C10.4443 48.4858 10.0547 51.8455 10.0547 51.8455C10.0547 51.8455 13.4332 52.0097 16.713 49.2282C19.9928 46.4467 21.9318 41.7734 21.9318 41.7734C21.9318 41.7734 17.0039 42.9228 13.7241 45.7043Z",
|
||||
p18c6ee00: "M25.0666 96.9892C25.0891 101.289 27.4122 103.748 27.4122 103.748C27.4122 103.748 29.7096 101.266 29.6871 96.9653C29.6646 92.6651 27.331 88.1758 27.331 88.1758C27.331 88.1758 25.0441 92.6889 25.0666 96.9892Z",
|
||||
p19c9c800: "M21.9921 25.2152C17.9589 26.7069 16.4434 29.7311 16.4434 29.7311C16.4434 29.7311 19.5621 31.0411 23.5953 29.549C27.6285 28.0573 31.0492 24.3283 31.0492 24.3283C31.0492 24.3283 26.0253 23.7235 21.9921 25.2152Z",
|
||||
p1aa3a000: "M21.6546 17.9522C21.6349 21.7464 19.5852 23.9149 19.5852 23.9149C19.5852 23.9149 17.5585 21.7248 17.5783 17.9307C17.598 14.137 19.6573 10.1758 19.6573 10.1758C19.6573 10.1758 21.6743 14.1581 21.6546 17.9522Z",
|
||||
p1aecd280: "M17.0492 93.8441C21.0824 95.3358 22.598 98.36 22.598 98.36C22.598 98.36 19.4792 99.67 15.446 98.1779C11.4128 96.6862 7.99213 92.9572 7.99213 92.9572C7.99213 92.9572 13.016 92.3524 17.0492 93.8441Z",
|
||||
p1bc39000: "M8.18402 48.3255C10.3149 52.061 9.53209 55.3513 9.53209 55.3513C9.53209 55.3513 6.3014 54.3501 4.17054 50.6147C2.03968 46.8793 1.81622 41.8242 1.81622 41.8242C1.81622 41.8242 6.05362 44.5901 8.18402 48.3255Z",
|
||||
p1d43d300: "M40.9732 111.576C42.4649 115.609 45.4891 117.125 45.4891 117.125C45.4891 117.125 46.7991 114.006 45.3069 109.973C43.8152 105.94 40.0862 102.52 40.0862 102.52C40.0862 102.52 39.481 107.543 40.9732 111.576Z",
|
||||
p1d45f300: "M7.40019 60.5347C10.1473 63.8434 9.94767 67.22 9.94767 67.22C9.94767 67.22 6.59217 66.7951 3.84509 63.4864C1.09801 60.1777 0 55.2383 0 55.2383C0 55.2383 4.65311 57.226 7.40019 60.5347Z",
|
||||
p1da3fb80: "M23.9388 102.719C28.1697 103.488 30.1873 106.202 30.1873 106.202C30.1873 106.202 27.3434 108.034 23.1124 107.265C18.8814 106.496 14.8652 103.418 14.8652 103.418C14.8652 103.418 19.7078 101.95 23.9388 102.719Z",
|
||||
p1e2f4600: "M34.3784 6.72824C33.3938 9.39043 31.3978 10.3907 31.3978 10.3907C31.3978 10.3907 30.5333 8.33235 31.518 5.67015C32.5027 3.00796 34.9639 0.75 34.9639 0.75C34.9639 0.75 35.3631 4.0665 34.3784 6.72824Z",
|
||||
p1f0e8880: "M11.9017 83.3634C15.6147 85.5328 16.5819 88.7741 16.5819 88.7741C16.5819 88.7741 13.2833 89.5224 9.57035 87.353C5.85741 85.1836 3.13649 80.9178 3.13649 80.9178C3.13649 80.9178 8.18878 81.1936 11.9017 83.3634Z",
|
||||
p201be680: "M32.3629 110.809C36.6631 110.831 39.1212 113.154 39.1212 113.154C39.1212 113.154 36.6388 115.452 32.3386 115.429C28.0383 115.407 23.549 113.073 23.549 113.073C23.549 113.073 28.0626 110.786 32.3629 110.809Z",
|
||||
p210c1400: "M42.0619 117.316C46.3006 116.591 49.1253 118.452 49.1253 118.452C49.1253 118.452 47.0793 121.146 42.8405 121.871C38.6018 122.595 33.7752 121.077 33.7752 121.077C33.7752 121.077 37.8231 118.04 42.0619 117.316Z",
|
||||
p256715b0: "M38.3342 8.18471C35.5361 7.7066 33.6719 8.93492 33.6719 8.93492C33.6719 8.93492 35.0222 10.7129 37.8203 11.191C40.6183 11.6692 43.804 10.6666 43.804 10.6666C43.804 10.6666 41.1317 8.66282 38.3342 8.18471Z",
|
||||
p25734a80: "M12.7717 67.2665C10.6408 71.0019 11.4236 74.2923 11.4236 74.2923C11.4236 74.2923 14.6543 73.2911 16.7852 69.5556C18.916 65.8202 19.1395 60.7656 19.1395 60.7656C19.1395 60.7656 14.9026 63.5311 12.7717 67.2665Z",
|
||||
p267b1ac0: "M27.7761 11.8017C27.2136 14.897 25.2277 16.3731 25.2277 16.3731C25.2277 16.3731 23.8879 14.2927 24.4504 11.1974C25.013 8.10202 27.2645 5.16406 27.2645 5.16406C27.2645 5.16406 28.3387 8.70631 27.7761 11.8017Z",
|
||||
p2748fff0: "M50.5804 116.582C52.7498 120.295 55.991 121.262 55.991 121.262C55.991 121.262 56.7394 117.963 54.57 114.25C52.4006 110.537 48.1348 107.816 48.1348 107.816C48.1348 107.816 48.4105 112.869 50.5804 116.582Z",
|
||||
p27baec00: "M23.9388 103.273C28.1697 104.042 30.1873 106.757 30.1873 106.757C30.1873 106.757 27.3434 108.588 23.1124 107.819C18.8814 107.05 14.8652 103.415 14.8652 103.415C14.8652 103.415 19.7078 102.505 23.9388 103.273Z",
|
||||
p28227200: "M12.7717 66.7123C10.6408 70.4477 11.4236 73.738 11.4236 73.738C11.4236 73.738 14.6543 72.7368 16.7852 69.0014C18.916 65.266 19.1395 60.2109 19.1395 60.2109C19.1395 60.2109 14.9026 62.9773 12.7717 66.7123Z",
|
||||
p2962ed00: "M27.2884 17.4154C23.5557 18.0936 21.7754 20.4887 21.7754 20.4887C21.7754 20.4887 24.2843 22.1043 28.017 21.4262C31.7497 20.748 35.2928 18.0321 35.2928 18.0321C35.2928 18.0321 31.021 16.7368 27.2884 17.4154Z",
|
||||
p29feda00: "M8.18402 48.8763C10.3149 52.6117 9.53209 55.9021 9.53209 55.9021C9.53209 55.9021 6.3014 54.9009 4.17054 51.1655C2.03968 47.43 1.81622 42.375 1.81622 42.375C1.81622 42.375 6.05362 45.1413 8.18402 48.8763Z",
|
||||
p2a3c4980: "M42.0619 117.871C46.3007 117.146 49.1253 119.007 49.1253 119.007C49.1253 119.007 47.0793 121.701 42.8406 122.425C38.6018 123.15 33.7752 121.074 33.7752 121.074C33.7752 121.074 37.8231 118.595 42.0619 117.871Z",
|
||||
p2b140300: "M12.3047 55.8897C9.55759 59.1984 9.75719 62.575 9.75719 62.575C9.75719 62.575 13.1127 62.1502 15.8598 58.8414C18.6068 55.5327 19.7044 50.5938 19.7044 50.5938C19.7044 50.5938 15.0517 52.5815 12.3047 55.8897Z",
|
||||
p2b52f600: "M32.3742 12.6251C29.2283 12.6416 27.4297 14.3412 27.4297 14.3412C27.4297 14.3412 29.2458 16.0219 32.3916 16.0054C35.5374 15.9889 38.8218 14.2815 38.8218 14.2815C38.8218 14.2815 35.52 12.6086 32.3742 12.6251Z",
|
||||
p2b737f00: "M11.9017 82.8088C15.6147 84.9782 16.5819 88.2194 16.5819 88.2194C16.5819 88.2194 13.2833 88.9678 9.57035 86.7984C5.85741 84.629 3.13649 80.3632 3.13649 80.3632C3.13649 80.3632 8.18878 80.6394 11.9017 82.8088Z",
|
||||
p2bef8b80: "M7.40019 59.9795C10.1473 63.2882 9.94767 66.6648 9.94767 66.6648C9.94767 66.6648 6.59217 66.24 3.84509 62.9313C1.09801 59.6226 0 54.6836 0 54.6836C0 54.6836 4.65311 56.6708 7.40019 59.9795Z",
|
||||
p2c576380: "M15.7 26.3023C16.4245 30.5411 14.5634 33.3657 14.5634 33.3657C14.5634 33.3657 11.87 31.3197 11.1451 27.0809C10.4206 22.8422 11.9393 18.0156 11.9393 18.0156C11.9393 18.0156 14.9755 22.0635 15.7 26.3023Z",
|
||||
p2c8fde00: "M25.0666 96.4345C25.0891 100.735 27.4122 103.193 27.4122 103.193C27.4122 103.193 29.7096 100.711 29.6871 96.4107C29.6646 92.1104 27.331 87.6211 27.331 87.6211C27.331 87.6211 25.0441 92.1343 25.0666 96.4345Z",
|
||||
p2d41e180: "M40.9732 111.021C42.4649 115.055 45.4891 116.57 45.4891 116.57C45.4891 116.57 46.7991 113.451 45.3069 109.419C43.8152 105.386 40.0862 101.965 40.0862 101.965C40.0862 101.965 39.481 106.988 40.9732 111.021Z",
|
||||
p2e1d1300: "M41.3529 5.13071C38.871 6.71508 36.5938 6.29158 36.5938 6.29158C36.5938 6.29158 37.1687 4.04784 39.6505 2.46392C42.1329 0.879547 45.582 0.554688 45.582 0.554688C45.582 0.554688 43.8352 3.54679 41.3529 5.13071Z",
|
||||
p2ef99880: "M17.0492 93.2896C21.0824 94.7813 22.598 97.8055 22.598 97.8055C22.598 97.8055 19.4792 99.1155 15.446 97.6233C11.4128 96.1316 7.99213 92.4026 7.99213 92.4026C7.99213 92.4026 13.016 91.7974 17.0492 93.2896Z",
|
||||
p2f70ae00: "M12.3047 56.4449C9.55759 59.7536 9.75719 63.1302 9.75719 63.1302C9.75719 63.1302 13.1127 62.7053 15.8598 59.3966C18.6068 56.0879 19.7044 51.1485 19.7044 51.1485C19.7044 51.1485 15.0517 53.1366 12.3047 56.4449Z",
|
||||
p2f8c8f00: "M23.9387 103.273C28.1697 104.042 30.1872 106.757 30.1872 106.757C30.1872 106.757 27.3433 108.588 23.1123 107.819C18.8814 107.05 14.8651 103.415 14.8651 103.415C14.8651 103.415 19.7078 102.505 23.9387 103.273Z",
|
||||
p30863900: "M15.1127 77.2893C13.6633 81.3376 15.0054 84.4426 15.0054 84.4426C15.0054 84.4426 18.0135 82.8954 19.463 78.847C20.9125 74.7987 20.255 69.7812 20.255 69.7812C20.255 69.7812 16.5627 73.2405 15.1127 77.2893Z",
|
||||
p310ac280: "M32.3629 110.809C36.6631 110.831 39.1211 113.154 39.1211 113.154C39.1211 113.154 36.6388 115.452 32.3385 115.429C28.0383 115.407 23.549 113.073 23.549 113.073C23.549 113.073 28.0626 110.786 32.3629 110.809Z",
|
||||
p3120dcf1: "M34.3784 6.17355C33.3938 8.83575 31.3978 9.83602 31.3978 9.83602C31.3978 9.83602 30.5333 7.77766 31.518 5.11546C32.5027 2.45327 34.9639 0.195312 34.9639 0.195312C34.9639 0.195312 35.3631 3.51136 34.3784 6.17355Z",
|
||||
p34a39c00: "M23.9387 102.719C28.1697 103.488 30.1872 106.202 30.1872 106.202C30.1872 106.202 27.3433 108.034 23.1123 107.265C18.8814 106.496 14.8651 103.418 14.8651 103.418C14.8651 103.418 19.7078 101.95 23.9387 102.719Z",
|
||||
p35597700: "M42.0619 117.871C46.3006 117.146 49.1253 119.007 49.1253 119.007C49.1253 119.007 47.0793 121.701 42.8405 122.425C38.6018 123.15 33.7752 121.074 33.7752 121.074C33.7752 121.074 37.8231 118.595 42.0619 117.871Z",
|
||||
p384dfbb2: "M21.9921 25.7657C17.9589 27.2574 16.4434 30.2816 16.4434 30.2816C16.4434 30.2816 19.5621 31.5916 23.5953 30.0995C27.6285 28.6078 31.0492 24.8793 31.0492 24.8793C31.0492 24.8793 26.0253 24.274 21.9921 25.7657Z",
|
||||
p3a37f880: "M32.3829 104.425C33.1519 108.656 35.8665 110.673 35.8665 110.673C35.8665 110.673 37.6977 107.829 36.9291 103.599C36.1601 99.3679 33.0822 95.3517 33.0822 95.3517C33.0822 95.3517 31.6144 100.194 32.3829 104.425Z",
|
||||
p3ab77300: "M32.3742 12.0704C29.2283 12.0869 27.4297 13.7865 27.4297 13.7865C27.4297 13.7865 29.2458 15.4672 32.3916 15.4507C35.5374 15.4342 38.8218 13.7268 38.8218 13.7268C38.8218 13.7268 35.52 12.0544 32.3742 12.0704Z",
|
||||
p3ac9e600: "M32.3829 104.979C33.1519 109.21 35.8665 111.228 35.8665 111.228C35.8665 111.228 37.6977 108.384 36.9291 104.153C36.1601 99.9225 33.0822 95.9062 33.0822 95.9062C33.0822 95.9062 31.6144 100.748 32.3829 104.979Z",
|
||||
p3b910e80: "M16.9849 34.8206C13.2719 36.99 12.3047 40.2313 12.3047 40.2313C12.3047 40.2313 15.6033 40.9796 19.3162 38.8102C23.0292 36.6408 25.7501 32.375 25.7501 32.375C25.7501 32.375 20.6978 32.6512 16.9849 34.8206Z",
|
||||
p3bd1ec80: "M61.129 123.054C61.129 123.054 60.555 122.939 59.4432 122.678C58.3443 122.416 56.737 122.009 54.7268 121.372C50.7193 120.099 45.0595 117.868 38.9876 113.937C35.963 111.977 32.8337 109.597 29.8168 106.774C26.8008 103.957 23.8757 100.689 21.2539 97.0282C18.6284 93.3722 16.2938 89.311 14.4112 84.9773C12.5258 80.644 11.0901 76.0331 10.188 71.3235C9.27769 66.6122 8.90832 61.8044 9.03634 57.0912C9.16206 52.3748 9.80306 47.7538 10.8657 43.393C11.9266 39.0308 13.41 34.9324 15.1568 31.2121C16.9032 27.4905 18.9046 24.1414 20.9777 21.2199C25.127 15.3559 29.5135 11.22 32.7272 8.5674C34.3382 7.23768 35.6643 6.26861 36.576 5.62164C37.49 4.97697 37.9883 4.64844 37.9883 4.64844L38.1824 4.93567C38.1824 4.93567 37.6896 5.2665 36.7861 5.91576C35.885 6.56731 34.575 7.54189 32.9855 8.87758C29.815 11.5407 25.4931 15.6891 21.4319 21.5365C19.3997 24.4538 17.4474 27.7873 15.7501 31.4874C14.0528 35.1852 12.6213 39.2515 11.6104 43.5715C10.5978 47.8891 10.005 52.4583 9.92144 57.1109C9.83518 61.764 10.2417 66.4997 11.1773 71.1276C12.106 75.7596 13.5578 80.2792 15.4473 84.5216C17.3363 88.7668 19.6576 92.7252 22.2679 96.2946C24.8751 99.8671 27.7589 103.034 30.7436 105.774C33.7266 108.515 36.7967 110.809 39.7778 112.705C45.7455 116.496 51.2878 118.623 55.2086 119.831C57.1743 120.436 58.7449 120.819 59.8176 121.065C60.8799 121.305 61.4736 121.419 61.4736 121.419L61.129 123.054Z",
|
||||
p3cdab7f0: "M15.7 26.857C16.4245 31.0958 14.5634 33.9204 14.5634 33.9204C14.5634 33.9204 11.87 31.8744 11.1451 27.6356C10.4206 23.3969 11.9393 18.5703 11.9393 18.5703C11.9393 18.5703 14.9755 22.6182 15.7 26.857Z",
|
||||
p3d17ff00: "M27.2882 17.4154C23.5555 18.0936 21.7752 20.4887 21.7752 20.4887C21.7752 20.4887 24.2841 22.1043 28.0168 21.4262C31.7495 20.748 35.2926 18.0321 35.2926 18.0321C35.2926 18.0321 31.0208 16.7368 27.2882 17.4154Z",
|
||||
p3d25f400: "M38.3342 7.62612C35.5361 7.14801 33.6719 8.37632 33.6719 8.37632C33.6719 8.37632 35.0222 10.1543 37.8203 10.6324C40.6183 11.1106 43.804 10.1084 43.804 10.1084C43.804 10.1084 41.1317 8.10469 38.3342 7.62612Z",
|
||||
p3d89d680: "M10.9785 36.9841C12.428 41.0325 11.0859 44.1375 11.0859 44.1375C11.0859 44.1375 8.07818 42.5902 6.62824 38.5419C5.17876 34.4935 5.83628 29.4766 5.83628 29.4766C5.83628 29.4766 9.52903 32.9353 10.9785 36.9841Z",
|
||||
p3e657540: "M13.7241 45.1497C10.4443 47.9312 10.0547 51.2908 10.0547 51.2908C10.0547 51.2908 13.4332 51.4551 16.713 48.6736C19.9928 45.8921 21.9318 41.2188 21.9318 41.2188C21.9318 41.2188 17.0039 42.3687 13.7241 45.1497Z",
|
||||
p4373e00: "M42.0619 117.316C46.3007 116.591 49.1253 118.452 49.1253 118.452C49.1253 118.452 47.0793 121.146 42.8406 121.871C38.6018 122.595 33.7752 121.077 33.7752 121.077C33.7752 121.077 37.8231 118.04 42.0619 117.316Z",
|
||||
p5059280: "M50.5019 121.935C54.5502 120.486 57.6552 121.828 57.6552 121.828C57.6552 121.828 56.108 124.836 52.0596 126.285C48.0113 127.735 42.9943 126.52 42.9943 126.52C42.9943 126.52 46.4535 123.384 50.5019 121.935Z",
|
||||
p5369080: "M16.9849 35.3758C13.2719 37.5452 12.3047 40.7864 12.3047 40.7864C12.3047 40.7864 15.6033 41.5348 19.3162 39.3654C23.0292 37.196 25.7501 32.9297 25.7501 32.9297C25.7501 32.9297 20.6978 33.2064 16.9849 35.3758Z",
|
||||
p56ddb00: "M32.3629 111.367C36.6631 111.39 39.1212 113.713 39.1212 113.713C39.1212 113.713 36.6388 116.01 32.3386 115.988C28.0383 115.965 23.549 113.075 23.549 113.075C23.549 113.075 28.0626 111.345 32.3629 111.367Z",
|
||||
p5885870: "M10.9785 37.5393C12.428 41.5876 11.0859 44.6926 11.0859 44.6926C11.0859 44.6926 8.07818 43.1454 6.62824 39.097C5.17876 35.0487 5.83628 30.0312 5.83628 30.0312C5.83628 30.0312 9.52903 33.4909 10.9785 37.5393Z",
|
||||
paebf580: "M50.5804 116.027C52.7498 119.74 55.991 120.707 55.991 120.707C55.991 120.707 56.7394 117.409 54.57 113.696C52.4006 109.983 48.1348 107.262 48.1348 107.262C48.1348 107.262 48.4105 112.314 50.5804 116.027Z",
|
||||
pbdaf080: "M27.7761 11.247C27.2136 14.3423 25.2277 15.8184 25.2277 15.8184C25.2277 15.8184 23.8879 13.738 24.4504 10.6427C25.013 7.54733 27.2645 4.60938 27.2645 4.60938C27.2645 4.60938 28.3387 8.15163 27.7761 11.247Z",
|
||||
pe41c400: "M19.2514 87.8492C18.5269 92.0879 20.388 94.9126 20.388 94.9126C20.388 94.9126 23.0814 92.8666 23.8064 88.6278C24.5309 84.389 23.0121 79.5625 23.0121 79.5625C23.0121 79.5625 19.9764 83.6104 19.2514 87.8492Z",
|
||||
pf605580: "M8.65261 71.5911C11.9324 74.3726 12.3224 77.7323 12.3224 77.7323C12.3224 77.7323 8.94398 77.8965 5.66418 75.115C2.38439 72.3335 0.445324 67.6602 0.445324 67.6602C0.445324 67.6602 5.37282 68.8096 8.65261 71.5911Z",
|
||||
}
|
||||
16
imports/svg-wbq66h6oh8.ts
Normal file
16
imports/svg-wbq66h6oh8.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
p12200e80: "M29.9454 75.1666V70.3241H33.1263V70.8639H30.5491V72.4215H33.0799V72.9613H30.5491V74.6346H33.1263V75.1743H29.9454V75.1666Z",
|
||||
p160b7e80: "M44.5097 3.25365C44.649 3.30762 44.7264 3.40016 44.7806 3.51582L45.2139 4.4103L45.2913 4.57995C45.1288 4.57995 44.9818 4.57224 44.8347 4.57995C44.7109 4.58766 44.6567 4.54139 44.6103 4.43344L44.2852 3.67775C44.1924 3.47727 44.0144 3.39245 43.7977 3.43871V4.57224H43.2791V1.79626C43.2946 1.79626 43.3101 1.78855 43.3256 1.78855H44.1769C44.293 1.78855 44.4168 1.80397 44.5252 1.82711C44.9508 1.91964 45.1288 2.22037 45.0901 2.62134C45.0669 2.94521 44.8889 3.13798 44.5793 3.2228C44.5638 3.23823 44.5406 3.23823 44.5097 3.25365ZM43.7977 3.02232L44.2311 2.98376C44.293 2.97605 44.3549 2.94521 44.4013 2.90665C44.5406 2.81412 44.5638 2.67532 44.5406 2.5211C44.5174 2.3823 44.4323 2.30519 44.3007 2.25892C44.1382 2.21266 43.9679 2.22808 43.7977 2.22808V3.02232ZM4.04083 75.1668L3.60743 74.0873H1.19277L0.759367 75.1668H0.0705687L2.02861 70.3243H2.77933L4.72963 75.1668H4.04083ZM2.4001 70.9411L1.37077 73.5552H3.42943L2.4001 70.9411ZM5.36425 75.1668V70.3243H7.31456C8.28971 70.3243 8.8392 70.9951 8.8392 71.7817C8.8392 72.5759 8.28197 73.239 7.31456 73.239H5.96792V75.1668H5.36425ZM8.21232 71.7817C8.21232 71.2342 7.81762 70.8563 7.24491 70.8563H5.96792V72.6993H7.24491C7.81762 72.6993 8.21232 72.3214 8.21232 71.7817ZM9.64409 75.1668V70.3243H11.5944C12.5696 70.3243 13.119 70.9951 13.119 71.7817C13.119 72.5759 12.5618 73.239 11.5944 73.239H10.2478V75.1668H9.64409ZM12.4922 71.7817C12.4922 71.2342 12.0975 70.8563 11.5247 70.8563H10.2478V72.6993H11.5247C12.0975 72.6993 12.4922 72.3214 12.4922 71.7817ZM13.6298 74.4805L13.9936 74.0179C14.182 74.2367 14.416 74.4121 14.6793 74.532C14.9425 74.6518 15.2288 74.7132 15.5182 74.7118C16.385 74.7118 16.6636 74.2492 16.6636 73.8713C16.6636 72.6222 13.7846 73.3161 13.7846 71.5889C13.7846 70.7869 14.4966 70.2394 15.4641 70.2394C16.1993 70.2394 16.772 70.4939 17.1822 70.918L16.8184 71.3575C16.4547 70.9489 15.9439 70.7792 15.4099 70.7792C14.8372 70.7792 14.4115 71.0877 14.4115 71.5503C14.4115 72.6376 17.2905 72.013 17.2905 73.8251C17.2905 74.5191 16.8107 75.2516 15.495 75.2516C14.6592 75.2516 14.0323 74.9278 13.6298 74.4805ZM20.0844 75.3056V70.17H20.4869V75.3056H20.0844ZM27.2201 75.1668L26.152 71.2573L25.084 75.1668H24.4262L23.0408 70.3243H23.7142L24.7899 74.3571L25.9044 70.3243H26.4074L27.5219 74.3571L28.5899 70.3243H29.271L27.8779 75.1668H27.2201Z",
|
||||
p23e6b300: "M48.8432 75.1678V70.3253H52.0241V70.8651H49.4469V72.4227H51.9777V72.9625H49.4469V74.6358H52.0241V75.1756H48.8432V75.1678Z",
|
||||
p280e9a00: "M38.0009 1.21828C39.3089 3.33882 40.6013 5.32055 40.4078 7.87291C40.2685 9.70813 39.4636 11.2503 38.4498 12.7C37.939 13.4249 36.7781 13.34 36.306 12.5766L34.0307 8.6363C33.9378 8.46666 33.9687 8.15051 34.0693 7.97315L38.0009 1.21828ZM44.1072 0.686219C45.4848 0.686219 46.607 1.80432 46.607 3.17688C46.605 3.83816 46.3399 4.47165 45.8699 4.93852C45.3998 5.40539 44.7632 5.66755 44.0995 5.66755C42.7219 5.66755 41.5997 4.54174 41.5997 3.17688C41.6038 2.51624 41.8695 1.88392 42.3391 1.41749C42.8087 0.951053 43.4442 0.68824 44.1072 0.686219ZM41.8783 3.1846C41.8783 3.77358 42.1131 4.33845 42.5311 4.75492C42.9491 5.1714 43.5161 5.40538 44.1072 5.40538C45.33 5.40538 46.3284 4.41065 46.3362 3.1846C46.3362 2.59561 46.1013 2.03074 45.6833 1.61427C45.2653 1.19779 44.6984 0.963817 44.1072 0.963817C43.5161 0.963817 42.9491 1.19779 42.5311 1.61427C42.1131 2.03074 41.8783 2.59561 41.8783 3.1846Z",
|
||||
p2bb50bf0: "M66.8455 75.1666V70.3241H70.0264V70.8639H67.4492V72.4215H69.98V72.9613H67.4492V74.6346H70.0264V75.1743H66.8455V75.1666Z",
|
||||
p2dde0400: "M52.6973 60.9404L52.2252 60.0459L35.934 31.7541C34.9511 30.0422 33.1401 29.8418 31.6928 31.276C29.6806 33.2809 26.8635 33.4428 24.7816 31.677C23.1409 30.289 22.5914 27.6827 23.6439 25.8551L32.0488 11.4355C32.0953 11.3507 32.1649 11.289 32.2887 11.1271L64.5771 67.2095H60.8468L54.0594 67.1401L45.4997 67.1246C45.0509 67.1246 44.7955 67.0013 44.571 66.608L28.512 38.7017C28.0476 37.8998 28.0476 37.1056 28.5274 36.3422C29.015 35.5633 29.7735 35.2935 30.679 35.3397L32.5983 35.3706C34.1307 35.3243 35.2219 36.0029 35.9727 37.3138L47.4887 57.3239L48.2859 58.7273C49.2069 60.4777 50.5767 61.1948 52.6973 60.9404ZM20.8113 30.8056L41.7152 67.1169H33.9217C33.6741 67.1169 33.4264 67.1092 33.1865 67.1092L7.42235 67.0629L0.712367 66.9858C0.0158296 66.9781 -0.169914 66.7082 0.162876 66.1376L9.16369 50.7078C9.18691 50.6693 9.23335 50.6384 9.36491 50.5151C10.2162 52.3657 11.7022 53.8539 12.0427 55.9205C12.2362 57.1157 11.9653 58.2569 11.3075 59.2902L10.4252 60.8247H18.4122L18.002 60.0305L11.2456 48.3174C10.9979 47.8856 10.9824 47.5617 11.2378 47.1299L14.6896 41.231C14.7824 41.069 14.8985 40.9148 15.0533 40.6758L15.5409 41.4469L25.5401 58.843C26.2056 59.9996 27.1885 60.709 28.512 60.9095C28.9067 60.9712 29.3246 60.9172 29.8818 60.9172L29.4175 60.0613L17.05 38.6015C16.7405 38.0694 16.725 37.6839 17.05 37.1518L20.3934 31.4457L20.8113 30.8056ZM76.0004 67.2095C73.2993 67.2095 70.7221 67.2249 68.145 67.1863C67.8973 67.1786 67.5722 66.8547 67.4252 66.5926L59.0822 52.1421L41.3979 21.4367C39.7494 18.5913 39.672 15.7922 41.3824 12.97C41.9397 12.0524 42.4737 11.1193 43.0928 10.0475L76.0004 67.2095Z",
|
||||
p2e932e00: "M52.6973 60.9404C50.5767 61.1948 49.2069 60.4777 48.2782 58.7273L47.481 57.3239L35.9649 37.3137C35.2142 36.0029 34.123 35.3243 32.5906 35.3706C31.956 35.386 31.3136 35.3706 30.6712 35.3397C29.7657 35.2935 29.0073 35.5633 28.5197 36.3422C28.0399 37.1056 28.0476 37.8998 28.5042 38.7017L44.5788 66.608C44.8109 67.009 45.0663 67.1323 45.5075 67.1246L54.0672 67.14L60.8545 67.2094H64.5849L32.3042 11.1348C32.1804 11.289 32.1107 11.3584 32.0643 11.4432L23.6594 25.8629C22.6069 27.6981 23.1564 30.2967 24.7971 31.6847C26.879 33.4505 29.6883 33.2886 31.7083 31.2837C33.1555 29.8495 34.9588 30.0499 35.9494 31.7618L52.2407 60.0536L52.6973 60.9404ZM20.8113 30.8056L20.3934 31.4534L17.05 37.1595C16.725 37.6916 16.7405 38.0771 17.05 38.6092L29.4097 60.069L29.8741 60.925C29.3091 60.925 28.8989 60.9712 28.5042 60.9172C27.1808 60.7245 26.1979 60.015 25.5323 58.8507L15.5331 41.4546L15.0456 40.6835L14.6818 41.2387L11.2301 47.1376C10.967 47.5771 10.9824 47.8933 11.2378 48.3251L17.9942 60.0382C18.1335 60.2772 18.2419 60.524 18.4044 60.8324H10.4175L11.2997 59.2979C11.9576 58.2569 12.2362 57.1234 12.035 55.9282C11.7022 53.8616 10.2162 52.3734 9.35717 50.5228C9.22561 50.6461 9.17917 50.6693 9.15595 50.7155L0.162876 66.1453C-0.169914 66.7159 0.0158296 66.9781 0.712367 66.9935L7.42235 67.0706L33.1788 67.1092L33.914 67.1169H41.7075L20.8113 30.8056ZM76.0004 67.2094L43.0928 10.0475L41.3824 12.97C39.6798 15.7999 39.7494 18.5913 41.3979 21.4367L59.0822 52.1421L67.4252 66.5926C67.5722 66.847 67.8973 67.1786 68.145 67.1863L76.0004 67.2094Z",
|
||||
p33eaf500: "M70.7998 72.7462C70.7998 71.312 71.775 70.2401 73.2145 70.2401C74.654 70.2401 75.6369 71.312 75.6369 72.7462C75.6369 74.1728 74.654 75.2523 73.2145 75.2523C71.775 75.2523 70.7998 74.1805 70.7998 72.7462ZM75.0023 72.7462C75.0023 71.6127 74.298 70.7799 73.2067 70.7799C72.1155 70.7799 71.419 71.6127 71.419 72.7462C71.419 73.872 72.1078 74.7125 73.2067 74.7125C74.298 74.7125 75.0023 73.872 75.0023 72.7462Z",
|
||||
p340aab00: "M59.1536 75.3063V70.1708H59.556V75.3063H59.1536ZM62.288 74.4813L62.6517 74.0186C62.969 74.381 63.4876 74.7126 64.1764 74.7126C65.0432 74.7126 65.3218 74.2499 65.3218 73.8721C65.3218 72.6229 62.4428 73.3169 62.4428 71.5896C62.4428 70.7877 63.1548 70.2402 64.1222 70.2402C64.8574 70.2402 65.4301 70.4946 65.8403 70.9188L65.4766 71.3583C65.1128 70.9496 64.602 70.78 64.068 70.78C63.4953 70.78 63.0696 71.0884 63.0696 71.5511C63.0696 72.6383 65.9487 72.0137 65.9487 73.8258C65.9487 74.5198 65.4688 75.2524 64.1532 75.2524C63.3096 75.2524 62.6827 74.9285 62.288 74.4813Z",
|
||||
p3cb8f80: "M38.3796 74.4812L38.7434 74.0185C39.0607 74.381 39.5792 74.7125 40.268 74.7125C41.1348 74.7125 41.4134 74.2499 41.4134 73.872C41.4134 72.6229 38.5344 73.3168 38.5344 71.5896C38.5344 70.7876 39.2464 70.2401 40.2138 70.2401C40.9491 70.2401 41.5218 70.4946 41.932 70.9187L41.5682 71.3582C41.2045 70.9496 40.6937 70.7799 40.1597 70.7799C39.5869 70.7799 39.1613 71.0884 39.1613 71.551C39.1613 72.6383 42.0403 72.0137 42.0403 73.8258C42.0403 74.5198 41.5605 75.2523 40.2448 75.2523C39.4089 75.2523 38.7821 74.9284 38.3796 74.4812Z",
|
||||
p3d04c980: "M52.6977 74.4812L53.0615 74.0185C53.3788 74.381 53.8973 74.7125 54.5861 74.7125C55.4529 74.7125 55.7316 74.2499 55.7316 73.872C55.7316 72.6229 52.8525 73.3168 52.8525 71.5896C52.8525 70.7876 53.5645 70.2401 54.532 70.2401C55.2672 70.2401 55.8399 70.4946 56.2501 70.9187L55.8863 71.3582C55.5226 70.9496 55.0118 70.7799 54.4778 70.7799C53.9051 70.7799 53.4794 71.0884 53.4794 71.551C53.4794 72.6383 56.3584 72.0137 56.3584 73.8258C56.3584 74.5198 55.8786 75.2523 54.5629 75.2523C53.7271 75.2523 53.1002 74.9284 52.6977 74.4812Z",
|
||||
p3f37ee80: "M34.092 75.1678V70.3253H36.259C37.1025 70.3253 37.6211 70.8342 37.6211 71.5591C37.6211 72.1683 37.2109 72.5769 36.7698 72.6695C37.2883 72.7466 37.714 73.2786 37.714 73.857C37.714 74.6358 37.1954 75.1601 36.3054 75.1601H34.092V75.1678ZM36.9865 71.6439C36.9865 71.2121 36.6924 70.8574 36.1506 70.8574H34.6956V72.415H36.1506C36.7001 72.4227 36.9865 72.0834 36.9865 71.6439ZM37.0871 73.7953C37.0871 73.3557 36.7775 72.9625 36.1893 72.9625H34.6956V74.6358H36.1893C36.7465 74.6281 37.0871 74.3042 37.0871 73.7953Z",
|
||||
p63abc40: "M42.9447 75.1678V70.3253H43.5483V75.1678H42.9447ZM45.8856 75.1678V70.8651H44.3455V70.3253H48.0294V70.8651H46.4815V75.1678H45.8856Z",
|
||||
pd29a8f2: "M38.0009 1.21792L34.0694 7.97279C33.9688 8.15014 33.9378 8.46629 34.0307 8.63594L36.306 12.5763C36.7781 13.3474 37.939 13.4245 38.4498 12.6997C39.4637 11.25 40.2763 9.70006 40.4079 7.87254C40.6013 5.3279 39.3089 3.33845 38.0009 1.21792Z",
|
||||
}
|
||||
33
index.html
Normal file
33
index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WDI - Web Development & AI Solutions</title>
|
||||
<meta name="description" content="Leading web development and AI solutions provider. Custom software development, mobile apps, and digital transformation services." />
|
||||
|
||||
<!-- Preload Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta property="og:title" content="WDI - Web Development & AI Solutions" />
|
||||
<meta property="og:description" content="Leading web development and AI solutions provider. Custom software development, mobile apps, and digital transformation services." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://wdi.com" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="WDI - Web Development & AI Solutions" />
|
||||
<meta name="twitter:description" content="Leading web development and AI solutions provider. Custom software development, mobile apps, and digital transformation services." />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
install-and-run.md
Normal file
77
install-and-run.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Complete Local Development Setup
|
||||
|
||||
## Step 1: Install Dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Step 2: Start Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Step 3: If you still get figma:asset errors, run these manual fixes:
|
||||
|
||||
### Fix CarouselTestimonials.tsx
|
||||
Replace the import line in `components/CarouselTestimonials.tsx`:
|
||||
|
||||
```tsx
|
||||
// OLD:
|
||||
import clutchLogo from "figma:asset/2e527c2f1a28e8f4cafbb9fd9f8f9d410530d352.png";
|
||||
|
||||
// NEW:
|
||||
const clutchLogo = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### Fix CaseStudyHighlight.tsx
|
||||
Replace the imports in `components/CaseStudyHighlight.tsx`:
|
||||
|
||||
```tsx
|
||||
// OLD:
|
||||
import regroupImage from "figma:asset/92c9546d073e10bfa567559041d8b7e5b0d84ce7.png";
|
||||
import seezunImage from "figma:asset/06e3cfb0c62c4da1116eaa2ecf65c8d2c54cf50a.png";
|
||||
import wokaAwardImage from "figma:asset/91ae572d9f4dbf6bf5424e541b65db8087a129ff.png";
|
||||
|
||||
// NEW:
|
||||
const regroupImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";
|
||||
const seezunImage = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=600&h=400&fit=crop&auto=format";
|
||||
const wokaAwardImage = "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=600&h=400&fit=crop&auto=format";
|
||||
```
|
||||
|
||||
### Quick Fix Script (PowerShell)
|
||||
If you're on Windows, you can run this PowerShell script to fix all files at once:
|
||||
|
||||
```powershell
|
||||
# Fix CarouselTestimonials.tsx
|
||||
(Get-Content "components/CarouselTestimonials.tsx") -replace 'import clutchLogo from "figma:asset/[^"]+";', 'const clutchLogo = "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=120&h=60&fit=crop&auto=format";' | Set-Content "components/CarouselTestimonials.tsx"
|
||||
|
||||
# Fix CaseStudyHighlight.tsx
|
||||
$content = Get-Content "components/CaseStudyHighlight.tsx"
|
||||
$content = $content -replace 'import regroupImage from "figma:asset/[^"]+";', 'const regroupImage = "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=600&h=400&fit=crop&auto=format";'
|
||||
$content = $content -replace 'import seezunImage from "figma:asset/[^"]+";', 'const seezunImage = "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=600&h=400&fit=crop&auto=format";'
|
||||
$content = $content -replace 'import wokaAwardImage from "figma:asset/[^"]+";', 'const wokaAwardImage = "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=600&h=400&fit=crop&auto=format";'
|
||||
$content | Set-Content "components/CaseStudyHighlight.tsx"
|
||||
```
|
||||
|
||||
### Quick Fix Script (Bash/Linux/Mac)
|
||||
```bash
|
||||
# Fix CarouselTestimonials.tsx
|
||||
sed -i 's/import clutchLogo from "figma:asset\/[^"]*";/const clutchLogo = "https:\/\/images.unsplash.com\/photo-1560472354-b33ff0c44a43?w=120\&h=60\&fit=crop\&auto=format";/' components/CarouselTestimonials.tsx
|
||||
|
||||
# Fix CaseStudyHighlight.tsx
|
||||
sed -i 's/import regroupImage from "figma:asset\/[^"]*";/const regroupImage = "https:\/\/images.unsplash.com\/photo-1551650975-87deedd944c3?w=600\&h=400\&fit=crop\&auto=format";/' components/CaseStudyHighlight.tsx
|
||||
sed -i 's/import seezunImage from "figma:asset\/[^"]*";/const seezunImage = "https:\/\/images.unsplash.com\/photo-1512941937669-90a1b58e7e9c?w=600\&h=400\&fit=crop\&auto=format";/' components/CaseStudyHighlight.tsx
|
||||
sed -i 's/import wokaAwardImage from "figma:asset\/[^"]*";/const wokaAwardImage = "https:\/\/images.unsplash.com\/photo-1517077304055-6e89abbf09b0?w=600\&h=400\&fit=crop\&auto=format";/' components/CaseStudyHighlight.tsx
|
||||
```
|
||||
|
||||
## After Fixes
|
||||
1. Save all files
|
||||
2. The development server should automatically reload
|
||||
3. Navigate to `http://localhost:3001` in your browser
|
||||
4. The site should now load without errors!
|
||||
|
||||
## Pro Tip
|
||||
Create a `/public/images` folder and add your actual project images there, then import them as:
|
||||
```tsx
|
||||
const projectImage = "/images/my-project.png";
|
||||
```
|
||||
10
main.tsx
Normal file
10
main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './styles/globals.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
6794
package-lock.json
generated
Normal file
6794
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
package.json
Normal file
82
package.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "wdi-website",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^0.2.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.344.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"react-responsive-masonry": "^2.7.1",
|
||||
"react-slick": "^0.30.3",
|
||||
"recharts": "^2.15.4",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/node": "^24.0.13",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^5.4.19"
|
||||
}
|
||||
}
|
||||
1335
pages/AIAutomationWorkflows.tsx
Normal file
1335
pages/AIAutomationWorkflows.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1288
pages/AIChatbotsVirtualAssistants.tsx
Normal file
1288
pages/AIChatbotsVirtualAssistants.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1321
pages/AIIntegrationDigitalProducts.tsx
Normal file
1321
pages/AIIntegrationDigitalProducts.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user