updated new fonts
This commit is contained in:
30 files changed
+530
-223
No files matched your search
@@ -76,16 +76,16 @@ Content models: `Organization` (singleton with social links/map/hours), `AboutUs
|
||||
|
||||
Read-only JSON endpoints (paginated, page size 20):
|
||||
|
||||
| Endpoint | Description |
|
||||
|---|---|
|
||||
| `/api/homepage/hero/`, `/api/homepage/stats/`, `/api/homepage/marquee/` | Homepage sections |
|
||||
| `/api/org/about/`, `vision/`, `mision/`, `values/`, `what-we-do/`, `partners/`, `sister-companies/`, `services/`, `organization/` | Organization content |
|
||||
| `/api/products/` | Products (with features) |
|
||||
| `/api/projects/`, `/api/events/` | Projects & events |
|
||||
| `/api/team/` | Team members |
|
||||
| `/api/testimonials/` | Testimonials |
|
||||
| `/api/career/` | Job listings (filters: `job_time`, `location`; search) |
|
||||
| `/api/contact/` | `POST` create (public); list requires admin |
|
||||
| Endpoint | Description |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| `/api/homepage/hero/`, `/api/homepage/stats/`, `/api/homepage/marquee/` | Homepage sections |
|
||||
| `/api/org/about/`, `vision/`, `mision/`, `values/`, `what-we-do/`, `partners/`, `sister-companies/`, `services/`, `organization/` | Organization content |
|
||||
| `/api/products/` | Products (with features) |
|
||||
| `/api/projects/`, `/api/events/` | Projects & events |
|
||||
| `/api/team/` | Team members |
|
||||
| `/api/testimonials/` | Testimonials |
|
||||
| `/api/career/` | Job listings (filters: `job_time`, `location`; search) |
|
||||
| `/api/contact/` | `POST` create (public); list requires admin |
|
||||
|
||||
### Website pages (frontend)
|
||||
|
||||
|
||||
@@ -57,8 +57,10 @@ class OrganizationAdmin(ImagePreviewMixin, SingletonAdmin):
|
||||
{
|
||||
"fields": (
|
||||
"address",
|
||||
("phone_number", "telephone_number"),
|
||||
("primary_email", "secondary_email"),
|
||||
"phone_number",
|
||||
"telephone_number",
|
||||
"primary_email",
|
||||
"secondary_email",
|
||||
"whatsapp_no",
|
||||
),
|
||||
},
|
||||
@@ -67,8 +69,10 @@ class OrganizationAdmin(ImagePreviewMixin, SingletonAdmin):
|
||||
"Social Media",
|
||||
{
|
||||
"fields": (
|
||||
("facebook", "tiktok"),
|
||||
("instagram", "youtube"),
|
||||
"facebook",
|
||||
"tiktok",
|
||||
"instagram",
|
||||
"youtube",
|
||||
"linkedin",
|
||||
),
|
||||
},
|
||||
|
||||
@@ -105,6 +105,7 @@ USE_TZ = True
|
||||
# Static files and media files
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Udhamshil Nepal" />
|
||||
<meta name="keywords" content="machinery nepal, agro processing, food production machinery, cold storage nepal, construction machinery, small scale machinery nepal, commercial machinery nepal, udhamsil nepal, banke machinery" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="icon" type="image/png" href="{% static 'frontend/logo.png' %}" />
|
||||
<title>{{ seo.title|default:"Udhamshil Nepal | Machinery built for Nepal" }}</title>
|
||||
<meta name="description" content="{{ seo.description|default:'Reliable, innovative machinery solutions — exporting, manufacturing, distributing and supplying small, medium and commercial scale equipment across the nation.' }}" />
|
||||
|
||||
|
||||
Binary file not shown.
Whitespace-only changes.
@@ -50,6 +50,7 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^13.4.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lenis": "^1.3.26",
|
||||
"lucide-react": "^0.575.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.14.0",
|
||||
|
||||
Generated
+21
@@ -113,6 +113,9 @@ importers:
|
||||
input-otp:
|
||||
specifier: ^1.4.2
|
||||
version: 1.5.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
lenis:
|
||||
specifier: ^1.3.26
|
||||
version: 1.3.26(react@19.3.0)
|
||||
lucide-react:
|
||||
specifier: ^0.575.0
|
||||
version: 0.575.0(react@19.3.0)
|
||||
@@ -2285,6 +2288,20 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
lenis@1.3.26:
|
||||
resolution: {integrity: sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': '>=3.0.0'
|
||||
react: '>=17.0.0'
|
||||
vue: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -4716,6 +4733,10 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
lenis@1.3.26(react@19.3.0):
|
||||
optionalDependencies:
|
||||
react: 19.3.0
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
+35
-1
@@ -1,5 +1,6 @@
|
||||
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import Lenis from "lenis";
|
||||
import Layout from "./pages/Layout";
|
||||
import About from "./pages/About";
|
||||
import CareerDetail from "./pages/CareerDetail";
|
||||
@@ -10,23 +11,54 @@ import Index from "./pages/Home";
|
||||
import Products from "./pages/Products";
|
||||
import ProjectsEvents from "./pages/Projects-events";
|
||||
import Team from "./pages/Team";
|
||||
import Privacy from "./pages/Privacy";
|
||||
import Terms from "./pages/Terms";
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
// If Lenis is active, use its scrollTo method to prevent conflicts
|
||||
if ((window as any).lenis) {
|
||||
(window as any).lenis.scrollTo(0, { immediate: true });
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function SmoothScroll() {
|
||||
useEffect(() => {
|
||||
const lenis = new Lenis();
|
||||
(window as any).lenis = lenis;
|
||||
|
||||
function raf(time: number) {
|
||||
lenis.raf(time);
|
||||
requestAnimationFrame(raf);
|
||||
}
|
||||
|
||||
requestAnimationFrame(raf);
|
||||
|
||||
return () => {
|
||||
lenis.destroy();
|
||||
delete (window as any).lenis;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
|
||||
export default function App() {
|
||||
const basename =
|
||||
import.meta.env.VITE_ROUTER_BASENAME ||
|
||||
(window.location.pathname.startsWith("/static/frontend") ? "/static/frontend" : "/");
|
||||
return (
|
||||
<BrowserRouter basename={basename}>
|
||||
<SmoothScroll />
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
@@ -40,6 +72,8 @@ export default function App() {
|
||||
<Route path="products" element={<Products />} />
|
||||
<Route path="projects-events" element={<ProjectsEvents />} />
|
||||
<Route path="team" element={<Team />} />
|
||||
<Route path="privacy" element={<Privacy />} />
|
||||
<Route path="terms" element={<Terms />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export function ContactForm({
|
||||
title = "Send an enquiry",
|
||||
subtitle = "We generally respond within 24 hours.",
|
||||
className = "",
|
||||
}: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSent(false);
|
||||
setError(false);
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || "/api";
|
||||
const res = await fetch(`${baseUrl}/contact/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
phone_no: data.phone_no,
|
||||
email: data.email,
|
||||
subject: data.subject || "Website Enquiry",
|
||||
message: data.message,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSent(true);
|
||||
(e.target as HTMLFormElement).reset();
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className={`flex flex-col gap-8 ${className}`}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-4xl font-display font-black tracking-tighter uppercase text-foreground">
|
||||
{title}
|
||||
</h3>
|
||||
{subtitle && (
|
||||
<p className="mt-4 text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="name"
|
||||
placeholder="John Doe"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="phone_no"
|
||||
type="tel"
|
||||
placeholder="+977 98..."
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="subject"
|
||||
placeholder="Machinery Quote"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
What are you looking for?
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
name="message"
|
||||
rows={4}
|
||||
placeholder="Tell us about your requirements..."
|
||||
className="w-full resize-none border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 inline-flex self-start items-center gap-2 text-sm font-bold uppercase tracking-widest text-background bg-foreground px-8 py-4 hover:bg-foreground/80 transition-colors"
|
||||
>
|
||||
Send Enquiry
|
||||
</button>
|
||||
|
||||
{sent && (
|
||||
<div className="mt-4 border border-border p-6 text-center bg-muted/30">
|
||||
<p className="text-sm font-bold uppercase tracking-widest text-foreground">
|
||||
Thanks — your enquiry has been sent successfully. We will get back to you shortly.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-4 border border-border p-6 text-center bg-red-500/10">
|
||||
<p className="text-sm font-bold uppercase tracking-widest text-red-600">
|
||||
There was an error sending your message. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,12 @@ export function Footer() {
|
||||
<div className="flex flex-col lg:flex-row gap-16 lg:gap-24 mb-32">
|
||||
{/* Logo and Copyright */}
|
||||
<div className="flex flex-col lg:w-[35%] shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={org?.logo || `${import.meta.env.BASE_URL}logo.png`}
|
||||
alt={org?.name || "UDHAMSIL"}
|
||||
className="w-48 lg:w-64 h-auto object-contain"
|
||||
/>
|
||||
<span className="font-display text-2xl font-bold tracking-tight">
|
||||
{org?.name || "UDHAMSIL"}
|
||||
</span>
|
||||
@@ -152,10 +157,10 @@ export function Footer() {
|
||||
Legal
|
||||
</h4>
|
||||
<nav className="flex flex-col gap-4 text-sm font-medium text-background/70">
|
||||
<Link to="#" className="hover:text-background transition-colors">
|
||||
<Link to="/privacy" className="hover:text-background transition-colors">
|
||||
Privacy
|
||||
</Link>
|
||||
<Link to="#" className="hover:text-background transition-colors">
|
||||
<Link to="/terms" className="hover:text-background transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Link, NavLink } from "react-router-dom";
|
||||
import { ArrowUpRight, Menu } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useApi } from "@/hooks/useApi";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -24,15 +25,19 @@ const links = [
|
||||
|
||||
export function Header() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: org } = useApi<any>("org/organization");
|
||||
const logoUrl = org?.logo || `${import.meta.env.BASE_URL}logo.png`;
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 inset-x-0 w-full z-50 bg-background border-b border-transparent transition-all duration-300 hover:border-border">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6">
|
||||
<nav className="flex h-24 items-center justify-between gap-4">
|
||||
<Link to="/" className="flex items-center gap-4">
|
||||
<span className="leading-tight">
|
||||
<span className="block font-display text-lg tracking-wide">UDHAMSIL</span>
|
||||
</span>
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={org?.name || "Udhamshil Nepal"}
|
||||
className="h-20 md:h-24 w-auto object-contain scale-110 md:scale-125 origin-left"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="hidden h-full items-center gap-8 text-xs font-medium uppercase tracking-widest text-muted-foreground lg:flex">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
|
||||
/** Translates its children vertically as the page scrolls. */
|
||||
import { motion } from "framer-motion"; /** Translates its children vertically as the page scrolls. */
|
||||
export function Parallax({
|
||||
speed = 0.15,
|
||||
className,
|
||||
@@ -71,36 +70,19 @@ export function Reveal({
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [shown, setShown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
setShown(true);
|
||||
io.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.15 },
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
<motion.div
|
||||
className={className}
|
||||
style={{
|
||||
opacity: shown ? 1 : 0,
|
||||
transform: shown ? "none" : "translateY(28px)",
|
||||
transition: `opacity .7s ease ${delay}ms, transform .7s ease ${delay}ms`,
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-40px" }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: delay / 1000,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -49,7 +49,11 @@ function About() {
|
||||
const mision = misionList?.[0];
|
||||
const values = valuesList || [];
|
||||
|
||||
if (!isLoading && !about && !vision && !mision && values.length === 0) {
|
||||
if (isLoading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
if (!about && !vision && !mision && values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function CareerDetail() {
|
||||
const { slug } = useParams();
|
||||
const { data: role, loading } = useApi<CareerRole>(`career/${slug}`);
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <div className="min-h-[150vh] bg-background" />;
|
||||
|
||||
if (!role) {
|
||||
return <RoleNotFound />;
|
||||
|
||||
@@ -18,7 +18,11 @@ export interface CareerRole {
|
||||
function Career() {
|
||||
const { data: roles, loading } = useApi<CareerRole[]>("career");
|
||||
|
||||
if (!loading && (!roles || roles.length === 0)) {
|
||||
if (loading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,44 +15,11 @@ interface Organization {
|
||||
google_map_link: string;
|
||||
}
|
||||
|
||||
import { ContactForm } from "@/components/site/ContactForm";
|
||||
|
||||
export default function Contact() {
|
||||
const [sent, setSent] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const { data: org } = useApi<Organization>("org/organization");
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSent(false);
|
||||
setError(false);
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || "/api";
|
||||
const res = await fetch(`${baseUrl}/contact/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
phone_no: data.phone_no,
|
||||
email: data.email,
|
||||
subject: data.subject || "Website Enquiry",
|
||||
message: data.message,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSent(true);
|
||||
(e.target as HTMLFormElement).reset();
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen">
|
||||
<LightField />
|
||||
@@ -67,107 +34,7 @@ export default function Contact() {
|
||||
<div className="mx-auto grid max-w-7xl gap-8 px-6 pb-24 lg:grid-cols-12 lg:gap-16">
|
||||
{/* Form */}
|
||||
<Reveal className="order-2 lg:order-1 lg:col-span-7">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-8 border-t-2 border-foreground pt-12"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-4xl font-display font-black tracking-tighter uppercase text-foreground">
|
||||
Send an enquiry
|
||||
</h3>
|
||||
<p className="mt-4 text-xs font-bold uppercase tracking-widest text-muted-foreground">
|
||||
We generally respond within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="name"
|
||||
placeholder="John Doe"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="phone_no"
|
||||
type="tel"
|
||||
placeholder="+977 98..."
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="subject"
|
||||
placeholder="Machinery Quote"
|
||||
className="w-full border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-xs font-bold uppercase tracking-widest text-foreground">
|
||||
What are you looking for?
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
name="message"
|
||||
rows={4}
|
||||
placeholder="Tell us about your requirements..."
|
||||
className="w-full resize-none border-b border-border bg-transparent px-0 py-3 text-base text-foreground outline-none transition placeholder:text-muted-foreground/30 focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 inline-flex self-start items-center gap-2 text-sm font-bold uppercase tracking-widest text-background bg-foreground px-8 py-4 hover:bg-foreground/80 transition-colors"
|
||||
>
|
||||
Send Enquiry
|
||||
</button>
|
||||
|
||||
{sent && (
|
||||
<div className="mt-4 border border-border p-6 text-center">
|
||||
<p className="text-sm font-bold uppercase tracking-widest text-foreground">
|
||||
Thanks — your enquiry has been sent successfully. We will get back to you
|
||||
shortly.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mt-4 border border-border p-6 text-center">
|
||||
<p className="text-sm font-bold uppercase tracking-widest text-red-600">
|
||||
There was an error sending your message. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<ContactForm className="border-t-2 border-foreground pt-12" />
|
||||
</Reveal>
|
||||
|
||||
{/* Contact Info Sidebar */}
|
||||
|
||||
+29
-28
@@ -7,6 +7,7 @@ import { OrbitalVisualization } from "@/components/site/OrbitalVisualization";
|
||||
import { ClientsMarquee } from "@/components/site/ClientsMarquee";
|
||||
import { TestimonialsMarquee } from "@/components/site/TestimonialsMarquee";
|
||||
import { useApi } from "@/hooks/useApi";
|
||||
import { ContactForm } from "@/components/site/ContactForm";
|
||||
|
||||
function AnimatedStat({ value }: { value: number }) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
@@ -40,8 +41,8 @@ function AnimatedStat({ value }: { value: number }) {
|
||||
export default Index;
|
||||
|
||||
function Index() {
|
||||
const { data: heroList } = useApi("homepage/hero");
|
||||
const { data: org } = useApi("org/organization");
|
||||
const { data: heroList, loading: heroLoading } = useApi("homepage/hero");
|
||||
const { data: org, loading: orgLoading } = useApi("org/organization");
|
||||
const { data: statsList } = useApi("homepage/stats");
|
||||
const { data: marqueeList } = useApi("homepage/marquee");
|
||||
const { data: whatWeDoList } = useApi("org/what-we-do");
|
||||
@@ -59,6 +60,10 @@ function Index() {
|
||||
const services = servicesList || [];
|
||||
const testimonials = testimonialsList || [];
|
||||
|
||||
if (heroLoading || orgLoading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative bg-background">
|
||||
<LightField />
|
||||
@@ -320,33 +325,29 @@ function Index() {
|
||||
|
||||
{testimonials.length > 0 && <TestimonialsMarquee testimonials={testimonials} />}
|
||||
|
||||
<section className="relative z-10 bg-brand text-white overflow-hidden">
|
||||
<div className="mx-auto max-w-7xl px-6 py-32 md:py-48 text-center">
|
||||
<section className="relative z-10 bg-background overflow-hidden border-t border-border">
|
||||
<div className="mx-auto max-w-7xl px-6 py-32 md:py-48">
|
||||
<Reveal>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-white/70 mb-8">
|
||||
Ready to scale
|
||||
</p>
|
||||
<h2 className="text-5xl font-display font-black tracking-tighter uppercase md:text-7xl lg:text-[7rem] leading-[0.9]">
|
||||
Need a machine
|
||||
<br />
|
||||
spec'd for your
|
||||
<br />
|
||||
output?
|
||||
</h2>
|
||||
<div className="mt-16 flex flex-col items-center justify-center gap-8 sm:flex-row">
|
||||
<Link
|
||||
to="/contact"
|
||||
className="inline-flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-brand bg-white px-8 py-4 hover:bg-white/90 transition-colors"
|
||||
>
|
||||
Get a Quote <ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/products"
|
||||
className="inline-flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-white hover:text-white/70 transition-colors border-b border-white/30 pb-1 hover:border-white"
|
||||
>
|
||||
Browse Catalogue
|
||||
</Link>
|
||||
<div className="grid lg:grid-cols-2 gap-16 lg:gap-24 items-start">
|
||||
<div>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-8">
|
||||
Ready to scale
|
||||
</p>
|
||||
<h2 className="text-5xl font-display font-black tracking-tighter uppercase md:text-7xl leading-[0.9]">
|
||||
Need a machine
|
||||
<br />
|
||||
spec'd for your
|
||||
<br />
|
||||
output?
|
||||
</h2>
|
||||
<div className="mt-12 hidden lg:block">
|
||||
<p className="text-sm font-medium text-muted-foreground max-w-sm leading-relaxed">
|
||||
Share your scale, target output, and location — we'll come back with a recommendation and price tailored to your exact needs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-muted p-8 md:p-12 border border-border">
|
||||
<ContactForm title="Get a Quote" subtitle="" />
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useLocation, useOutlet } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { Header } from "../components/site/Header";
|
||||
import { Footer } from "../components/site/Footer";
|
||||
import { FloatingActionButtons } from "../components/site/FloatingActionButtons";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
const outlet = useOutlet();
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen flex-col overflow-x-hidden bg-background">
|
||||
<Header />
|
||||
<main className="flex-1 pt-20">
|
||||
<Outlet />
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 15, filter: "blur(2px)" }}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
||||
className="flex flex-col min-h-full"
|
||||
>
|
||||
{outlet}
|
||||
</motion.div>
|
||||
</main>
|
||||
<Footer />
|
||||
<FloatingActionButtons />
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { LightField } from "@/components/site/Parallax";
|
||||
import { PageHero } from "@/components/site/PageHero";
|
||||
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-background">
|
||||
<LightField />
|
||||
<PageHero
|
||||
eyebrow="Legal"
|
||||
title="Privacy Policy"
|
||||
description="How we handle and protect your data."
|
||||
/>
|
||||
|
||||
<section className="relative z-10 border-t border-border mt-16">
|
||||
<div className="mx-auto max-w-4xl px-6 py-32">
|
||||
<div className="prose prose-invert prose-lg max-w-none">
|
||||
<h2 className="font-display font-black tracking-tight uppercase">
|
||||
1. Information We Collect
|
||||
</h2>
|
||||
<p>
|
||||
We collect information that you provide directly to us, including when you fill out a
|
||||
form, request a quote, or communicate with us. This may include your name, email
|
||||
address, phone number, and any other information you choose to provide.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
2. How We Use Your Information
|
||||
</h2>
|
||||
<p>
|
||||
We use the information we collect to provide, maintain, and improve our services,
|
||||
communicate with you, respond to your inquiries, and fulfill your requests for
|
||||
machinery and equipment quotes.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
3. Information Sharing
|
||||
</h2>
|
||||
<p>
|
||||
We do not sell, trade, or otherwise transfer your personally identifiable information
|
||||
to outside parties unless we provide users with advance notice. This does not include
|
||||
website hosting partners and other parties who assist us in operating our website or
|
||||
conducting our business.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
4. Data Security
|
||||
</h2>
|
||||
<p>
|
||||
We implement a variety of security measures to maintain the safety of your personal
|
||||
information. However, no method of transmission over the Internet or electronic
|
||||
storage is 100% secure, and we cannot guarantee absolute security.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
5. Contact Us
|
||||
</h2>
|
||||
<p>
|
||||
If there are any questions regarding this privacy policy, you may contact us using the
|
||||
information on our Contact page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,11 @@ interface Product {
|
||||
function Products() {
|
||||
const { data: products, loading } = useApi<Product[]>("products");
|
||||
|
||||
if (!loading && (!products || products.length === 0)) {
|
||||
if (loading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,11 @@ function ProjectsEvents() {
|
||||
|
||||
entries.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
if (!isLoading && entries.length === 0) {
|
||||
if (isLoading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,11 @@ interface TeamMember {
|
||||
function Team() {
|
||||
const { data: people, loading } = useApi<TeamMember[]>("team");
|
||||
|
||||
if (!loading && (!people || people.length === 0)) {
|
||||
if (loading) {
|
||||
return <div className="min-h-[150vh] bg-background" />;
|
||||
}
|
||||
|
||||
if (!people || people.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { LightField } from "@/components/site/Parallax";
|
||||
import { PageHero } from "@/components/site/PageHero";
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-background">
|
||||
<LightField />
|
||||
<PageHero
|
||||
eyebrow="Legal"
|
||||
title="Terms of Service"
|
||||
description="The rules that govern our relationship."
|
||||
/>
|
||||
|
||||
<section className="relative z-10 border-t border-border mt-16">
|
||||
<div className="mx-auto max-w-4xl px-6 py-32">
|
||||
<div className="prose prose-invert prose-lg max-w-none">
|
||||
<h2 className="font-display font-black tracking-tight uppercase">
|
||||
1. Acceptance of Terms
|
||||
</h2>
|
||||
<p>
|
||||
By accessing and using this website, you accept and agree to be bound by the terms and
|
||||
provision of this agreement.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
2. Use License
|
||||
</h2>
|
||||
<p>
|
||||
Permission is granted to temporarily download one copy of the materials on our website
|
||||
for personal, non-commercial transitory viewing only. This is the grant of a license,
|
||||
not a transfer of title.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
3. Disclaimer
|
||||
</h2>
|
||||
<p>
|
||||
The materials on our website are provided on an 'as is' basis. We make no warranties,
|
||||
expressed or implied, and hereby disclaim and negate all other warranties including,
|
||||
without limitation, implied warranties or conditions of merchantability, fitness for a
|
||||
particular purpose, or non-infringement of intellectual property or other violation of
|
||||
rights.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
4. Limitations
|
||||
</h2>
|
||||
<p>
|
||||
In no event shall we or our suppliers be liable for any damages (including, without
|
||||
limitation, damages for loss of data or profit, or due to business interruption)
|
||||
arising out of the use or inability to use the materials on our website.
|
||||
</p>
|
||||
|
||||
<h2 className="font-display font-black tracking-tight uppercase mt-12">
|
||||
5. Governing Law
|
||||
</h2>
|
||||
<p>
|
||||
These terms and conditions are governed by and construed in accordance with the laws
|
||||
of Nepal and you irrevocably submit to the exclusive jurisdiction of the courts in
|
||||
that location.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+66
-2
@@ -4,6 +4,54 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-Medium.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-Medium.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-SemiBold.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-SemiBold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-SemiBold.woff2') format('woff2');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Display';
|
||||
src: url('/static/frontend/fonts/InterDisplay-SemiBold.woff2') format('woff2');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
@@ -35,8 +83,8 @@
|
||||
--color-brand: var(--brand);
|
||||
--color-ink: var(--ink);
|
||||
--color-mist: var(--mist);
|
||||
--font-display: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-display: "Bricolage Grotesque", sans-serif;
|
||||
--font-sans: "Inter Display", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -283,3 +331,19 @@
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
html.lenis,
|
||||
html.lenis body {
|
||||
height: auto;
|
||||
}
|
||||
.lenis.lenis-smooth {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.lenis.lenis-stopped {
|
||||
overflow: hidden;
|
||||
}
|
||||
.lenis.lenis-smooth iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
Reference in new issue
Block a user