start new in php

main
suvodip ghosh 2025-07-17 13:07:37 +00:00
parent 65a37ad477
commit ac520dfcff
6 changed files with 344 additions and 302 deletions

View File

@ -1,21 +1,27 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Button } from '../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import Loader from "../ui/loader";
import { useToast } from "../ui/toast";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../ui/loader";
export default function Kubernetes() {
const { isLoggedIn, loading, balance } = useIsLoggedIn();
// Environment variables and hooks
const PUBLIC_DEPLOYMENT_CHANEL_1_URL = import.meta.env.PUBLIC_DEPLOYMENT_CHANEL_1_URL;
const { loading, isLoggedIn, balance } = useIsLoggedIn();
const { showToast } = useToast();
// State management
const [isLoading, setIsLoading] = useState(false);
const [deployError, setDeployError] = useState(null);
const [deployStatus, setDeployStatus] = useState({});
const [clusterStatus, setClusterStatus] = useState('');
const [productAmount, setProductAmount] = useState(100);
const [productAmount] = useState(100);
const [copied, setCopied] = useState(false);
const [downloadClusterId, setDownloadClusterId] = useState('');
// Form states
const [nodePools, setNodePools] = useState([{
label: `${getRandomString()}`,
size: '10215',
@ -28,7 +34,7 @@ export default function Kubernetes() {
cluster_label: `${getRandomString()}`,
});
// Generate random string for IDs
// Helper functions
function getRandomString(length = 8) {
return Math.random().toString(36).substring(2, length+2);
}
@ -40,58 +46,8 @@ export default function Kubernetes() {
{ id: '10218', name: '16 vCPU, 32GB RAM' }
];
// Check cluster status periodically after deployment
useEffect(() => {
if (!deployStatus.clusterId || deployStatus.isReady) return;
const checkStatus = async () => {
try {
const response = await fetch(
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=status&clusterId=${deployStatus.clusterId}&source=chanel_1`,
{ credentials: "include" }
);
if (!response.ok) throw new Error("Failed to check status");
const data = await response.json();
// Update status
setClusterStatus(data.status || 'pending');
if (data.status === 'active') {
// Cluster is ready for configuration download
setDeployStatus(prev => ({ ...prev, isReady: true }));
showToast({
title: "Cluster Ready",
description: "Your Kubernetes cluster is now fully configured.",
variant: "default"
});
return; // Stop checking when ready
} else if (data.status === 'failed') {
throw new Error("Cluster deployment failed");
}
// Continue checking every 20 seconds if not ready
setTimeout(checkStatus, 20000);
} catch (error) {
console.error("Status check error:", error);
// Retry after delay if not a fatal error
if (!error.message.includes('failed')) {
setTimeout(checkStatus, 30000);
} else {
setDeployError(error.message);
}
}
};
const timer = setTimeout(checkStatus, 15000);
return () => clearTimeout(timer);
}, [deployStatus.clusterId, deployStatus.isReady]);
const handleSubmit = async (e) => {
if(balance < productAmount){
return;
}
if (balance < productAmount) return;
e.preventDefault();
setIsLoading(true);
setDeployError(null);
@ -114,7 +70,7 @@ export default function Kubernetes() {
};
const response = await fetch(
"https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=deploy&source=chanel_1",
`${PUBLIC_DEPLOYMENT_CHANEL_1_URL}kubernetis/?query=deploy&source=chanel_1`,
{
method: "POST",
credentials: "include",
@ -130,13 +86,11 @@ export default function Kubernetes() {
if (data.status === "success") {
setDeployStatus({
status: "success",
clusterId: data.clusterId,
endpoint: data.endpoint,
isReady: false
clusterId: data.id
});
showToast({
title: "Deployment Started",
description: "Your cluster is being provisioned. This may take 10-15 minutes.",
title: "Deployment Successful",
description: "Your cluster is being provisioned.",
variant: "default"
});
} else {
@ -154,22 +108,26 @@ export default function Kubernetes() {
}
};
const downloadKubeConfig = async () => {
if (!deployStatus.clusterId) return;
try {
// Trigger download via backend
window.open(
`https://host-api.cs1.hz.siliconpin.com/v1/kubernetis/?query=download&clusterId=${deployStatus.clusterId}&source=chanel_1`,
'_blank'
);
} catch (error) {
const downloadKubeConfig = (clusterId) => {
if (!clusterId) {
showToast({
title: "Download Failed",
description: "Could not download configuration. Please try again.",
title: "Cluster ID Required",
description: "Please enter a valid Cluster ID",
variant: "destructive"
});
return;
}
window.open(
`${PUBLIC_DEPLOYMENT_CHANEL_1_URL}kubernetis/?query=download&clusterId=${clusterId}&source=chanel_1`,
'_blank'
);
};
const handleCopy = (text) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
// Form field handlers
@ -220,11 +178,9 @@ export default function Kubernetes() {
setNodePools(updatedPools);
}
};
if (loading) {
return <Loader />;
}
if (!isLoggedIn) {
return (
<p className="text-center mt-8">
@ -235,248 +191,231 @@ export default function Kubernetes() {
if (balance < productAmount) {
return (
<p>
You have insufficient balance to deploy this service. Please <a href="/profile" className="text-[#6d9e37]"> click here </a> to go to your profile and add balance, then try again.</p>
);
}
if (deployStatus.status === 'success') {
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Kubernetes Cluster Deployment</CardTitle>
<CardDescription>
{deployStatus.isReady
? "Your cluster is ready to use"
: "Your cluster is being provisioned"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>Current Status</Label>
<div className="p-3 bg-gray-100 rounded-md">
<div className="flex items-center gap-2">
{!deployStatus.isReady && <Loader size="sm" />}
<span className="font-medium">
{deployStatus.isReady ? 'Ready' : (clusterStatus || 'Starting deployment')}
</span>
</div>
{!deployStatus.isReady && (
<p className="text-sm text-muted-foreground mt-2">
This process may take several minutes. Please don't close this page.
</p>
)}
</div>
</div>
<div className="space-y-4">
<div>
<Label>Cluster ID</Label>
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
{deployStatus.clusterId}
</p>
</div>
<div>
<Label>API Endpoint</Label>
<p className="font-mono p-2 bg-gray-100 rounded text-sm">
{deployStatus.endpoint || 'Pending...'}
</p>
</div>
</div>
{deployStatus.isReady ? (
<div className="flex flex-col gap-4 pt-4">
<Button onClick={downloadKubeConfig}>
Download Configuration File
</Button>
<Button variant="outline" onClick={() => window.location.reload()}>
Deploy Another Cluster
</Button>
</div>
) : (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader size="sm" />
<span>Preparing your cluster... This may take several minutes</span>
</div>
)}
</CardContent>
</Card>
<p className="text-center mt-8">
You have insufficient balance to deploy this service. Please <a href="/profile" className="text-[#6d9e37]">click here</a> to add balance.
</p>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Create Kubernetes Cluster</CardTitle>
<CardDescription>
Configure your Kubernetes cluster with the desired specifications
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Name *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
<div className="space-y-6 max-w-3xl mx-auto mt-8">
{/* Deployment Card */}
{deployStatus.status === 'success' ? (
<Card>
<CardHeader>
<CardTitle>Kubernetes Cluster Deployment</CardTitle>
<CardDescription>Save your Cluster ID in a safe place to download configuration file</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex">
<Input
className="rounded-l-md rounded-r-none text-xl font-bold border-[2px] border-[#6d9e37]"
type="text"
readOnly
value={deployStatus.clusterId}
/>
<Button
className="rounded-l-none rounded-r-md"
onClick={() => handleCopy(deployStatus.clusterId)}
>
{copied ? 'Copied!' : 'Copy'}
</Button>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle>Create Kubernetes Cluster</CardTitle>
<CardDescription>
Configure your Kubernetes cluster with the desired specifications
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Name *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-lg">Node Pools</Label>
<Button
type="button"
onClick={addNodePool}
variant="outline"
size="sm"
>
Add Node Pool
</Button>
</div>
{nodePools.map((pool, poolIndex) => (
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
<div className="space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
{nodePools.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeNodePool(poolIndex)}
>
Remove
</Button>
)}
<Label className="text-lg">Node Pools</Label>
<Button
type="button"
onClick={addNodePool}
variant="outline"
size="sm"
>
Add Node Pool
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Pool Label</Label>
<Input
value={pool.label}
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
placeholder="worker-pool"
/>
</div>
{nodePools.map((pool, poolIndex) => (
<div key={poolIndex} className="p-4 border rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h4 className="font-medium">Node Pool #{poolIndex + 1}</h4>
{nodePools.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeNodePool(poolIndex)}
>
Remove
</Button>
)}
</div>
<div className="space-y-2">
<Label>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{availableSizes.map(size => (
<SelectItem key={size.id} value={size.id}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Node Count *</Label>
<Input
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-md">Storage Volumes</Label>
<Button
type="button"
onClick={() => addDisk(poolIndex)}
variant="outline"
size="sm"
>
Add Volume
</Button>
</div>
{pool.ebs.map((disk, diskIndex) => (
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Size (GB) *</Label>
<Label>Pool Label</Label>
<Input
type="number"
min="1"
value={disk.disk}
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
placeholder="30"
value={pool.label}
onChange={(e) => handleNodePoolChange(poolIndex, 'label', e.target.value)}
placeholder="worker-pool"
/>
</div>
<div className="space-y-2">
<Label>Type *</Label>
<Label>Node Size *</Label>
<Select
value={disk.type}
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
value={pool.size}
onValueChange={(value) => handleNodePoolChange(poolIndex, 'size', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
<SelectItem selected value="nvme">NVMe SSD</SelectItem>
{/* <SelectItem value="ssd">Standard SSD</SelectItem>
<SelectItem value="hdd">HDD</SelectItem> */}
{availableSizes.map(size => (
<SelectItem key={size.id} value={size.id}>
{size.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
{pool.ebs.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeDisk(poolIndex, diskIndex)}
>
Remove
</Button>
)}
<div className="space-y-2">
<Label>Node Count *</Label>
<Input
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(poolIndex, 'count', parseInt(e.target.value) || 1)}
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading}
className="w-full md:w-auto"
>
{isLoading ? (
<>
<Loader size="sm" className="mr-2" />
Deploying...
</>
) : (
"Deploy Cluster"
)}
</Button>
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-md">Storage Volumes</Label>
<Button
type="button"
onClick={() => addDisk(poolIndex)}
variant="outline"
size="sm"
>
Add Volume
</Button>
</div>
{pool.ebs.map((disk, diskIndex) => (
<div key={diskIndex} className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="space-y-2">
<Label>Size (GB) *</Label>
<Input
type="number"
min="1"
value={disk.disk}
onChange={(e) => handleDiskChange(poolIndex, diskIndex, 'disk', e.target.value)}
placeholder="30"
/>
</div>
<div className="space-y-2">
<Label>Type *</Label>
<Select
value={disk.type}
onValueChange={(value) => handleDiskChange(poolIndex, diskIndex, 'type', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem selected value="nvme">NVMe SSD</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
{pool.ebs.length > 1 && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeDisk(poolIndex, diskIndex)}
>
Remove
</Button>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading}
className="w-full md:w-auto"
>
{isLoading ? "Deploying..." : "Deploy Cluster"}
</Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* Download Config Card - Always visible */}
<Card>
<CardHeader>
<CardTitle>Download Configuration</CardTitle>
<CardDescription>
Enter your Cluster ID to download the kubeconfig file <br /> <span className="text-xs">(Try to download config file after 5 minutes from Deployment!)</span>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="downloadClusterId">Cluster ID</Label>
<Input
id="downloadClusterId"
value={deployStatus.clusterId || downloadClusterId}
onChange={(e) => setDownloadClusterId(e.target.value)}
placeholder="Enter your Cluster ID"
/>
</div>
</form>
</CardContent>
</Card>
<Button
className="w-full"
onClick={() => downloadKubeConfig(deployStatus.clusterId || downloadClusterId)}
>
Download Config
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,98 @@
import React, { useEffect, useState } from 'react';
import { Heart, Loader2 } from 'lucide-react';
export default function LikeSystem({ postId, siliconId, token }) {
const [liked, setLiked] = useState(false);
const [likeCount, setLikeCount] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchLikeStatusAndCount = async () => {
try {
const countRes = await fetch(`https://sp-likes-pocndhvgmcnbacgb.siliconpin.com/posts/${postId}/likes`);
const countData = await countRes.json();
setLikeCount(countData.likes);
const likedRes = await fetch(
`https://sp-likes-pocndhvgmcnbacgb.siliconpin.com/is-liked?post_id=${postId}`,
{
headers: {
'Authorization': `Bearer ${token}`,
'X-SiliconId': siliconId,
},
}
);
const likedData = await likedRes.json();
setLiked(likedData.liked);
} catch (err) {
console.error("Error fetching like data", err);
}
};
fetchLikeStatusAndCount();
}, [postId, token, siliconId]);
const handleLikeToggle = async () => {
setLoading(true);
try {
const res = await fetch(`https://sp-likes-pocndhvgmcnbacgb.siliconpin.com/like`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-SiliconId': siliconId,
},
body: JSON.stringify({ post_id: postId }),
});
const data = await res.json();
if (data.success) {
setLiked(data.liked);
setLikeCount(prev => data.liked ? prev + 1 : prev - 1);
}
} catch (error) {
console.error('Network error:', error);
} finally {
setLoading(false);
}
};
return (
<div style={{ textAlign: 'center' }}>
<button
onClick={handleLikeToggle}
disabled={loading}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px',
padding: '6px 14px',
backgroundColor: 'transparent',
border: 'none',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease-in-out',
}}
>
{loading ? (
<Loader2 size={22} className="animate-spin" />
) : (
<Heart
size={28}
fill={liked ? '#ff4d4f' : 'none'}
color={liked ? '#ff4d4f' : '#999'}
strokeWidth={2.2}
style={{ transition: '0.2s ease-in-out' }}
/>
)}
<span style={{
fontSize: '15px',
fontWeight: 'bold',
color: liked ? '#ff4d4f' : '#555'
}}>
{likeCount}
</span>
</button>
</div>
);
}

View File

@ -135,8 +135,8 @@ console.log('serviceName', serviceName)
</>
) : (
<>
<h2 className="text-xl font-bold mb-4 text-neutral-950">VPN Credentials</h2>
<p className="text-yellow-600 bg-yellow-50 p-3 rounded-md mb-4 text-sm"> Make sure to save these credentials in a safe place. We do not store them.</p>
<h2 className="text-xl font-bold mb-2 text-neutral-950">VPN Credentials</h2>
<p className="text-neutral-950 mb-4">Install WireGuard (Available on Android / IOS / Linux / Windows / FreeBSD) Scan this QR code using WireGuard app or import the config file.</p>
</>
)
}
@ -148,7 +148,6 @@ console.log('serviceName', serviceName)
{creds.qr_code && (
<div className="flex flex-col items-center">
<img src={`data:image/png;base64,${creds.qr_code}`} alt="VPN Configuration QR Code" className="w-48 h-48 border border-gray-300 rounded" />
<p className="text-sm text-neutral-400 mt-2">Scan this QR code with your WireGuard app</p>
</div>
)}
@ -161,6 +160,7 @@ console.log('serviceName', serviceName)
</Button>
</div>
<pre className="whitespace-pre-wrap bg-gray-50 p-3 rounded text-sm overflow-x-auto text-neutral-950">{creds.output}</pre>
<p className="text-yellow-600 bg-yellow-50 p-3 rounded-md my-4 text-sm"> Make sure to save these credentials in a safe place. We do not store them.</p>
</div>
)}
</div>

View File

@ -8,13 +8,16 @@ import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { token, user_name, pb_id } from '../lib/CookieValues';
import CommentSystem from './CommentSystem/CommentSystem';
import '../styles/markdown.css'
import '../styles/markdown.css';
import LikeSystem from './CommentSystem/LikeSystem'
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
export default function TopicDetail(props) {
// console.log('coockie data', user_name)
const [showCopied, setShowCopied] = useState(false);
const [allGalleryImages, setAllGalleryImages] = useState([]);
const siliconId = '4uPRIGMm';
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NTI5MjQxMTQsImlkIjoiZ203NzYyZ2Q2aTNscDAxIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Aq02zEAcXjyfsFO6MniM0atzzy2rSXx7B_rnilK6OYQ';
if (!props.topic) {
return <div>Topic not found</div>;
}
@ -126,10 +129,12 @@ export default function TopicDetail(props) {
<article className="max-w-4xl mx-auto">
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full object-cover rounded-lg mb-8 shadow-md" />
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
{/* Enhanced Social Share Buttons */}
<div className="mb-8">
<p className="text-sm text-gray-500 mb-3 font-medium">Share this Topic:</p>
<div className="flex flex-row justify-between">
<p className="text-sm text-gray-500 mb-3 font-medium">Share this Topic:</p>
<LikeSystem postId={props.topic.id} siliconId={siliconId} token={token} />
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => shareOnSocialMedia('facebook')}

View File

@ -435,7 +435,7 @@ export default function EditTopic (){
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
{/* <Label htmlFor="slug">URL Slug</Label> */}
<input
type="hidden"
name="slug"
@ -443,7 +443,7 @@ export default function EditTopic (){
onChange={handleChange}
className="bg-gray-50"
/>
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
{/* <p className="text-xs text-gray-500">This will be used in the topic URL</p> */}
</div>
{/* Featured Image Upload */}

View File

@ -43,8 +43,8 @@ const services = [
{
imageUrl: '/assets/images/services/kubernetes-edge.png',
learnMoreUrl: '/services/kubernetes',
buyButtonText: '',
buyButtonUrl: ''
buyButtonText: 'Get K8s',
buyButtonUrl: '/services/kubernetes'
},
{
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
@ -233,7 +233,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
newSchema1={JSON.stringify(schema1)}
canonicalURL="/services"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<main class="container mx-auto max-w-7xl px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">
<h1 class="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">Hosting Services</h1>
<p class="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">