Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91534f9735 | |||
| 36694248d0 | |||
| 0ea05a8ce2 | |||
| b2ab28d2a8 | |||
| e2f995e317 | |||
| 419b2761c8 | |||
| e7d4bdbf2c | |||
| fee0e95c25 | |||
| df5fd8cd08 | |||
| 42fc7ac14e | |||
| 7a39ed439f | |||
| 8542ac1e9e | |||
| efeb286ac5 |
@@ -1,57 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Roboto } from "next/font/google";
|
||||
import { Halant } from "next/font/google";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ServiceWrapper } from "@/components/ServiceWrapper";
|
||||
import Tag from "@/tag/Tag";
|
||||
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App", description: "Generated by create next app"};
|
||||
|
||||
const roboto = Roboto({
|
||||
variable: "--font-roboto", subsets: ["latin"],
|
||||
variable: "--font-roboto",
|
||||
subsets: ["latin"],
|
||||
weight: ["100", "300", "400", "500", "700", "900"],
|
||||
});
|
||||
|
||||
const halant = Halant({
|
||||
variable: "--font-halant", subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter", subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Luxury Landscape Design | Liberty Landscape", description: "Premium landscape design and build for high-end residential properties. Bespoke outdoor spaces with luxury stonework, garden architecture, and sophisticated lighting.", keywords: "luxury landscape design, high-end landscaping, premium outdoor design, landscape architect, luxury garden design, hardscape design", openGraph: {
|
||||
title: "Luxury Landscape Design | Liberty Landscape", description: "Transform your outdoor space into a refined, luxury environment. Custom landscape design and build from concept to completion.", siteName: "Liberty Landscape", images: [
|
||||
{
|
||||
url: "http://img.b2bpic.net/free-photo/big-stone-made-chinese-chess-inside-park_1112-1150.jpg", alt: "Luxury landscape design"
|
||||
}
|
||||
],
|
||||
type: "website"
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image", title: "Luxury Landscape Design | Liberty Landscape", description: "Premium landscape design and build for high-end properties.", images: ["http://img.b2bpic.net/free-photo/big-stone-made-chinese-chess-inside-park_1112-1150.jpg"]
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<ServiceWrapper>
|
||||
<body
|
||||
className={`${roboto.variable} ${halant.variable} ${inter.variable} antialiased`}
|
||||
>
|
||||
<Tag />
|
||||
{children}
|
||||
|
||||
<html lang="en">
|
||||
<body className={`${roboto.variable} antialiased`}>
|
||||
{children}
|
||||
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@@ -293,9 +265,7 @@ export default function RootLayout({
|
||||
const getElementInfo = (element, assignId = false) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
const selector = getUniqueSelector(element, assignId);
|
||||
const sectionId = getSectionId(element);
|
||||
|
||||
|
||||
let className = undefined;
|
||||
try {
|
||||
if (element.className) {
|
||||
@@ -323,8 +293,7 @@ export default function RootLayout({
|
||||
};
|
||||
|
||||
if (tagName === 'img') {
|
||||
const originalSrc = extractOriginalUrl(element.src);
|
||||
info.imageData = {
|
||||
info.imageData = {
|
||||
src: originalSrc,
|
||||
alt: element.alt || undefined,
|
||||
naturalWidth: element.naturalWidth,
|
||||
@@ -335,8 +304,7 @@ export default function RootLayout({
|
||||
|
||||
if (tagName === 'video') {
|
||||
const rawSrc = element.src || element.currentSrc || (element.querySelector('source') && element.querySelector('source').src) || '';
|
||||
const resolvedSrc = extractOriginalUrl(rawSrc);
|
||||
info.imageData = {
|
||||
info.imageData = {
|
||||
src: resolvedSrc,
|
||||
alt: element.getAttribute('aria-label') || undefined,
|
||||
isBackground: false,
|
||||
@@ -349,8 +317,7 @@ export default function RootLayout({
|
||||
if (backgroundImage && backgroundImage !== 'none') {
|
||||
const urlMatch = backgroundImage.match(/url(['"]?([^'")]+)['"]?)/);
|
||||
if (urlMatch) {
|
||||
const originalBgSrc = extractOriginalUrl(urlMatch[1]);
|
||||
if (tagName !== 'img') {
|
||||
if (tagName !== 'img') {
|
||||
info.imageData = {
|
||||
src: originalBgSrc,
|
||||
isBackground: true
|
||||
@@ -362,8 +329,7 @@ export default function RootLayout({
|
||||
}
|
||||
}
|
||||
|
||||
const elementType = getElementType(element);
|
||||
info.elementType = elementType;
|
||||
info.elementType = elementType;
|
||||
|
||||
if (elementType === 'Button') {
|
||||
const buttonText = element.textContent?.trim() || element.value || element.getAttribute('aria-label') || '';
|
||||
@@ -456,13 +422,11 @@ export default function RootLayout({
|
||||
};
|
||||
|
||||
const isTextElement = (element) => {
|
||||
const elementType = getElementType(element);
|
||||
return elementType === 'Text';
|
||||
return elementType === 'Text';
|
||||
};
|
||||
|
||||
const isButtonElement = (element) => {
|
||||
const elementType = getElementType(element);
|
||||
return elementType === 'Button';
|
||||
return elementType === 'Button';
|
||||
};
|
||||
|
||||
const updateButtonText = (element, newText) => {
|
||||
@@ -537,8 +501,7 @@ export default function RootLayout({
|
||||
};
|
||||
|
||||
const handleInput = () => {
|
||||
const elementInfo = getElementInfo(element);
|
||||
let currentText = element.textContent;
|
||||
let currentText = element.textContent;
|
||||
|
||||
// Ensure there's always at least a space to keep the element editable
|
||||
if (currentText === '' || currentText === null || currentText.length === 0) {
|
||||
@@ -651,8 +614,7 @@ export default function RootLayout({
|
||||
}, '*');
|
||||
|
||||
if (save && originalContent !== element.textContent) {
|
||||
const elementInfo = getElementInfo(element);
|
||||
let finalText = element.textContent;
|
||||
let finalText = element.textContent;
|
||||
|
||||
// Trim the final text and convert space-only to empty string for saving
|
||||
if (finalText === ' ' || finalText.trim() === '') {
|
||||
@@ -781,7 +743,7 @@ export default function RootLayout({
|
||||
lastMouseX = e.clientX;
|
||||
lastMouseY = e.clientY;
|
||||
|
||||
const target = getMostSpecificElement(e.clientX, e.clientY) || e.target;
|
||||
|| e.target;
|
||||
|
||||
if (!isValidElement(target) || target === hoveredElement || target === selectedElement) {
|
||||
return;
|
||||
@@ -813,8 +775,7 @@ export default function RootLayout({
|
||||
hoverOverlay = createHoverOverlay(target);
|
||||
}
|
||||
|
||||
const elementType = getElementType(target);
|
||||
showElementTypeLabel(target, elementType);
|
||||
showElementTypeLabel(target, elementType);
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'webild-element-hover',
|
||||
@@ -856,7 +817,7 @@ export default function RootLayout({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const target = getMostSpecificElement(e.clientX, e.clientY) || e.target;
|
||||
|| e.target;
|
||||
if (!isValidElement(target)) return;
|
||||
|
||||
if (selectedElement && selectedElement !== target) {
|
||||
@@ -901,8 +862,7 @@ export default function RootLayout({
|
||||
hoveredElement = null;
|
||||
}
|
||||
|
||||
const elementInfo = getElementInfo(target, true);
|
||||
selectedElement.dataset.webildSelector = elementInfo.selector;
|
||||
selectedElement.dataset.webildSelector = elementInfo.selector;
|
||||
showElementTypeLabel(target, elementInfo.elementType);
|
||||
|
||||
window.parent.postMessage({
|
||||
@@ -985,8 +945,7 @@ export default function RootLayout({
|
||||
isScrolling = false;
|
||||
|
||||
if (lastMouseX > 0 && lastMouseY > 0) {
|
||||
const target = getMostSpecificElement(lastMouseX, lastMouseY);
|
||||
if (target && isValidElement(target) && target !== selectedElement) {
|
||||
if (target && isValidElement(target) && target !== selectedElement) {
|
||||
hoveredElement = target;
|
||||
|
||||
const computedStyle = window.getComputedStyle(target);
|
||||
@@ -1000,8 +959,7 @@ export default function RootLayout({
|
||||
hoveredElement.classList.add(hoverClass);
|
||||
hoverOverlay = createHoverOverlay(target);
|
||||
|
||||
const elementType = getElementType(target);
|
||||
showElementTypeLabel(target, elementType);
|
||||
showElementTypeLabel(target, elementType);
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'webild-element-hover',
|
||||
@@ -1024,8 +982,7 @@ export default function RootLayout({
|
||||
|
||||
const saveChangeToStorage = (change) => {
|
||||
try {
|
||||
const storageKey = getStorageKey();
|
||||
const existingChanges = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
||||
const existingChanges = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
||||
|
||||
const filteredChanges = existingChanges.filter(c => {
|
||||
return !(c.oldValue === change.oldValue && c.sectionId === change.sectionId);
|
||||
@@ -1045,8 +1002,7 @@ export default function RootLayout({
|
||||
|
||||
const clearLocalChanges = () => {
|
||||
try {
|
||||
const storageKey = getStorageKey();
|
||||
localStorage.removeItem(storageKey);
|
||||
localStorage.removeItem(storageKey);
|
||||
window.parent.postMessage({
|
||||
type: 'webild-local-changes-cleared',
|
||||
data: {}
|
||||
@@ -1095,8 +1051,7 @@ export default function RootLayout({
|
||||
|
||||
if (e.data.type === 'webild-cancel-changes') {
|
||||
try {
|
||||
const storageKey = getStorageKey();
|
||||
const savedChanges = localStorage.getItem(storageKey);
|
||||
const savedChanges = localStorage.getItem(storageKey);
|
||||
if (savedChanges) {
|
||||
const changes = JSON.parse(savedChanges);
|
||||
changes.forEach(change => {
|
||||
@@ -1118,8 +1073,7 @@ export default function RootLayout({
|
||||
if (isBackground) {
|
||||
element.style.backgroundImage = change.oldValue ? 'url(' + change.oldValue + ')' : '';
|
||||
} else {
|
||||
const oldMediaType = getMediaTypeFromUrl(change.oldValue);
|
||||
if (revertTag === 'video' && oldMediaType === 'image') {
|
||||
if (revertTag === 'video' && oldMediaType === 'image') {
|
||||
swapMediaElement(element, 'img', change.oldValue);
|
||||
} else if (revertTag === 'img' && oldMediaType === 'video') {
|
||||
swapMediaElement(element, 'video', change.oldValue);
|
||||
@@ -1167,8 +1121,7 @@ export default function RootLayout({
|
||||
const el = textElements[i];
|
||||
if (isTextElement(el) && el.textContent.trim() === (oldValue || '').trim()) {
|
||||
element = el;
|
||||
const newSelector = getUniqueSelector(element, true);
|
||||
if (newSelector) {
|
||||
if (newSelector) {
|
||||
element.dataset.webildSelector = newSelector;
|
||||
}
|
||||
break;
|
||||
@@ -1259,10 +1212,8 @@ export default function RootLayout({
|
||||
replaced = true;
|
||||
} else if (element.tagName.toLowerCase() === 'img') {
|
||||
oldValue = element.src;
|
||||
const newMediaType = getMediaTypeFromUrl(newSrc);
|
||||
if (newMediaType === 'video' && allowMediaTypeSwap) {
|
||||
const swapped = swapMediaElement(element, 'video', newSrc);
|
||||
if (selectedElement === element) selectedElement = swapped;
|
||||
if (newMediaType === 'video' && allowMediaTypeSwap) {
|
||||
if (selectedElement === element) selectedElement = swapped;
|
||||
element = swapped;
|
||||
} else {
|
||||
element.src = newSrc;
|
||||
@@ -1270,11 +1221,9 @@ export default function RootLayout({
|
||||
replaced = true;
|
||||
} else if (element.tagName.toLowerCase() === 'video') {
|
||||
oldValue = element.src || element.currentSrc || '';
|
||||
const newMediaType = getMediaTypeFromUrl(newSrc);
|
||||
const sources = element.querySelectorAll('source');
|
||||
const sources = element.querySelectorAll('source');
|
||||
if (newMediaType === 'image' && allowMediaTypeSwap) {
|
||||
const swapped = swapMediaElement(element, 'img', newSrc);
|
||||
if (selectedElement === element) selectedElement = swapped;
|
||||
if (selectedElement === element) selectedElement = swapped;
|
||||
element = swapped;
|
||||
} else {
|
||||
if (sources.length > 0) {
|
||||
@@ -1296,8 +1245,7 @@ export default function RootLayout({
|
||||
}
|
||||
|
||||
if (replaced) {
|
||||
const elementInfo = getElementInfo(element);
|
||||
|
||||
|
||||
let cleanOldValue = oldValue;
|
||||
if (oldValue.includes('url(')) {
|
||||
const urlMatch = oldValue.match(/url(['"]?([^'")]+)['"]?)/);
|
||||
@@ -1368,13 +1316,7 @@ export default function RootLayout({
|
||||
}
|
||||
}, true);
|
||||
|
||||
const urlCheckInterval = setInterval(() => {
|
||||
if (lastPathname !== window.location.pathname) {
|
||||
lastPathname = window.location.pathname;
|
||||
notifyPageChange();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
|
||||
notifyPageChange();
|
||||
|
||||
window.webildCleanup = () => {
|
||||
@@ -1419,7 +1361,6 @@ export default function RootLayout({
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</ServiceWrapper>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function LandingPage() {
|
||||
tag="Premium Landscape Design"
|
||||
tagAnimation="slide-up"
|
||||
background={{ variant: "canvas-reveal" }}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/big-stone-made-chinese-chess-inside-park_1112-1150.jpg?_wi=1"
|
||||
imageSrc="https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772583870794-6p6pr9p7.jpg"
|
||||
imageAlt="Luxury outdoor landscape with modern stonework and lush greenery"
|
||||
buttons={[
|
||||
{ text: "Book a Consultation", href: "#contact" },
|
||||
@@ -74,23 +74,23 @@ export default function LandingPage() {
|
||||
features={[
|
||||
{
|
||||
id: 1,
|
||||
title: "Landscape Design & Build", description: "Bespoke outdoor environments, designed with intention and built with precision.", phoneOne: { imageSrc: "http://img.b2bpic.net/free-vector/landscape-garden-design-concept-poster_1284-5948.jpg?_wi=1" },
|
||||
phoneTwo: { imageSrc: "http://img.b2bpic.net/free-vector/landscape-garden-design-concept-poster_1284-5948.jpg?_wi=2" }
|
||||
title: "Before & After ", description: "Bespoke outdoor environments, designed with intention and built with precision.", phoneOne: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772583988900-ec0n3ogb.jpg" },
|
||||
phoneTwo: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772583999338-narcnxtd.jpg" }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Luxury Stonework & Hardscapes", description: "Premium patios, walkways, terraces, retaining walls, and custom architectural features.", phoneOne: { imageSrc: "http://img.b2bpic.net/free-photo/big-stone-made-chinese-chess-inside-park_1112-1150.jpg?_wi=2" },
|
||||
phoneTwo: { imageSrc: "http://img.b2bpic.net/free-photo/big-stone-made-chinese-chess-inside-park_1112-1150.jpg?_wi=3" }
|
||||
title: "Luxury Stonework & Hardscapes", description: "Premium patios, walkways, terraces, retaining walls, and custom architectural features.", phoneOne: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584082925-k2zz5auk.jpg" },
|
||||
phoneTwo: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584091428-fpnv6j9c.jpg" }
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Garden Architecture & Planting", description: "High-end planting design with layered textures, colors, and seasonal interest.", phoneOne: { imageSrc: "http://img.b2bpic.net/free-photo/pond_23-2148013427.jpg?_wi=1" },
|
||||
phoneTwo: { imageSrc: "http://img.b2bpic.net/free-photo/pond_23-2148013427.jpg?_wi=2" }
|
||||
title: "Garden Architecture & Planting", description: "High-end planting design with layered textures, colors, and seasonal interest.", phoneOne: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584120735-n40uukf0.jpg" },
|
||||
phoneTwo: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584142889-vf4f7q0n.jpg" }
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Outdoor Lighting & Enhancements", description: "Sophisticated lighting that transforms your property day and night.", phoneOne: { imageSrc: "http://img.b2bpic.net/free-photo/outside-view-restaurant-cottage-night-time_140725-8844.jpg?_wi=1" },
|
||||
phoneTwo: { imageSrc: "http://img.b2bpic.net/free-photo/outside-view-restaurant-cottage-night-time_140725-8844.jpg?_wi=2" }
|
||||
title: "Lawn Maintenance & Install ", description: "Transform your yard into what you invision.", phoneOne: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584190676-iybsenvb.jpg" },
|
||||
phoneTwo: { imageSrc: "https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AP6m7l7hwi4GIZuXGy8tnhucL3/uploaded-1772584242286-rej9ffn9.jpg" }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -108,16 +108,16 @@ export default function LandingPage() {
|
||||
useInvertedBackground={true}
|
||||
metrics={[
|
||||
{
|
||||
id: "1", value: "Unmatched", description: "Craftsmanship with luxury-grade materials and flawless execution"
|
||||
id: "1", value: "Quick ", description: "Craftsmanship with luxury-grade materials and flawless execution"
|
||||
},
|
||||
{
|
||||
id: "2", value: "Design-Led", description: "Every property receives a custom blueprint shaped by your lifestyle"
|
||||
id: "2", value: "Desire", description: "Every property receives a custom blueprint shaped by your lifestyle"
|
||||
},
|
||||
{
|
||||
id: "3", value: "White-Glove", description: "Clear communication, on-time delivery, exceptional care throughout"
|
||||
id: "3", value: "", description: "Clear communication, on-time delivery, exceptional care throughout"
|
||||
},
|
||||
{
|
||||
id: "4", value: "Built to Last", description: "Projects designed to age beautifully and endure for generations"
|
||||
id: "4", value: "Beauty ", description: "Projects designed to age beautifully and endure for generations"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -153,7 +153,7 @@ export default function LandingPage() {
|
||||
<TextSplitAbout
|
||||
title="Crafting Luxury Outdoor Spaces With Purpose"
|
||||
description={[
|
||||
"Liberty Landscape was founded on a simple belief: outdoor environments should be as thoughtfully designed as the interiors they surround.", "We partner with homeowners who value quality, craft, and timeless design—delivering spaces that feel like an extension of the home, built with precision and passion."
|
||||
"Liberty Landscape was founded on a simple belief: outdoor environments should be as thoughtfully designed as the interiors they surround.", "With 20+ years of combined landscape design expertise and 500+ completed luxury projects, we partner with homeowners who value quality, craft, and timeless design—delivering spaces that feel like an extension of the home, built with precision and passion."
|
||||
]}
|
||||
buttons={[
|
||||
{ text: "Meet the Team", href: "#contact" }
|
||||
|
||||
Reference in New Issue
Block a user