start new in php
parent
65a37ad477
commit
ac520dfcff
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue