updated siliconpin

pull/32/head
suvodip ghosh 2025-05-27 05:59:30 +00:00
parent 9091219729
commit 4e79dde03e
61 changed files with 8676 additions and 1143 deletions

42
facebook.json Normal file
View File

@ -0,0 +1,42 @@
{
"meta": {
"expiry": "2025-06-29 11:36:07.506Z",
"rawUser": {
"email": "suvodipghosh35@gmail.com",
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"picture": {
"data": {
"height": 201,
"is_silhouette": false,
"url": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"width": 200
}
}
},
"id": "3913660032212288",
"name": "Suvodip Ghosh",
"username": "",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g",
"accessToken": "EAATKn4EGuGABO5BQDFZBCNX6QrVyfsUZBCfJKov5OzHHrQIAEu5wOePaLcvIZBklT9MPkoiZCrZCraJNIwtBzGrKxZCeUMwKJRAbicZAPuK1gesmDl27iL3J7vp0sAjMoue4yACIZAIRDZBzsDU0Yi91ztFJ6FOJ2qWVfLkTgjkKwEudTe95vUYcN1E9oS6bo6smA75KdVB6RgGiK217C4ez9aRDrQcvH3ZBSGZBY2QZBW1KynycojdqhKAQS68xT1Bpezv80kEP5DQfONMMNbaUOwBU9r0WxwS0viCbW0qZAk92mXYZAZBQhny5Ili4aP1jmAZD",
"refreshToken": "",
"avatarUrl": "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=3913660032212288&height=200&width=200&ext=1748604969&hash=AbaH-X9tWREaBN46FPCtAG6g"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3NzAsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.5Xz_0hoAod9YLgeOTHbWTEKdRxqusZPUDyb2D0SfVKQ"
}

77
github.json Normal file
View File

@ -0,0 +1,77 @@
{
"meta": {
"expiry": "",
"rawUser": {
"avatar_url": "https://avatars.githubusercontent.com/u/128118374?v=4",
"bio": null,
"blog": "",
"collaborators": 0,
"company": null,
"created_at": "2023-03-17T04:49:12Z",
"disk_usage": 55453,
"email": null,
"events_url": "https://api.github.com/users/suvodip35/events{/privacy}",
"followers": 0,
"followers_url": "https://api.github.com/users/suvodip35/followers",
"following": 1,
"following_url": "https://api.github.com/users/suvodip35/following{/other_user}",
"gists_url": "https://api.github.com/users/suvodip35/gists{/gist_id}",
"gravatar_id": "",
"hireable": null,
"html_url": "https://github.com/suvodip35",
"id": 128118374,
"location": null,
"login": "suvodip35",
"name": "Suvodip",
"node_id": "U_kgDOB6LuZg",
"notification_email": null,
"organizations_url": "https://api.github.com/users/suvodip35/orgs",
"owned_private_repos": 4,
"plan": {
"collaborators": 0,
"name": "free",
"private_repos": 10000,
"space": 976562499
},
"private_gists": 0,
"public_gists": 0,
"public_repos": 4,
"received_events_url": "https://api.github.com/users/suvodip35/received_events",
"repos_url": "https://api.github.com/users/suvodip35/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/suvodip35/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/suvodip35/subscriptions",
"total_private_repos": 4,
"twitter_username": null,
"two_factor_authentication": true,
"type": "User",
"updated_at": "2025-04-30T08:46:03Z",
"url": "https://api.github.com/users/suvodip35",
"user_view_type": "private"
},
"id": "128118374",
"name": "Suvodip",
"username": "suvodip35",
"email": "suvodipghosh35@gmail.com",
"avatarURL": "https://avatars.githubusercontent.com/u/128118374?v=4",
"accessToken": "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",
"refreshToken": "",
"avatarUrl": "https://avatars.githubusercontent.com/u/128118374?v=4"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc2NDQsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.XLlZwZ3TrHWlIoxOhkLoKW7AbTPr6VIB49fRpNCTVbY"
}

38
google.json Normal file
View File

@ -0,0 +1,38 @@
{
"meta": {
"expiry": "2025-04-30 12:35:11.096Z",
"rawUser": {
"email": "suvoairtel@gmail.com",
"family_name": "GHOSH",
"given_name": "SUBHODIP",
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"picture": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"verified_email": true
},
"id": "112026834578335832361",
"name": "SUBHODIP GHOSH",
"username": "",
"email": "suvoairtel@gmail.com",
"avatarURL": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c",
"accessToken": "ya29.a0AZYkNZiV6bkpVlDOQy2ayy6WCULOE5uWYi_QvRSN9vUqJgUxuxcn3xlUzbe6rOHQV7UCjPb_aCmiOojHCMWsCvtky80pUBNCI2rGGhCZYjpSMTispb9qvPyO7CNgsqh5YGxTUUXAkuz37uXm68MFv5YOoofMZ1f39f2OU0sIhisaCgYKAbgSARMSFQHGX2MiEUQ1pMH6u7K92FgWTPp4Uw0178",
"refreshToken": "",
"avatarUrl": "https://lh3.googleusercontent.com/a/ACg8ocLvTh2_Injp6f23coUiAfTQB_WmLki0vmQinLak3pnY_xnznQ=s96-c"
},
"record": {
"avatar": "colorful_bird_sits_branch_forest_uiu589q5aa.jpg",
"collectionId": "_pb_users_auth_",
"collectionName": "users",
"created": "2025-03-10 13:17:51.909Z",
"email": "neha@siliconpin.com",
"emailVisibility": true,
"id": "7fp08mzhs7qgmg9",
"name": "Neha Ghosh",
"phone": "9090935312",
"provider": "",
"type": "admin",
"updated": "2025-04-30 09:34:24.970Z",
"verified": true
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3NDY2MTc3MTMsImlkIjoiN2ZwMDhtemhzN3FnbWc5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.Wig_LpSxOd03z389n186t2AwUu1Ypxmj3Apf5LdciaQ"
}

2016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@
"build": "astro build",
"preview": "astro preview --port 4000",
"astro": "astro",
"push-s33": "rsync -rv --exclude .hta_config/conf.php dist/ dev2@siliconpin.s33.siliconpin.com:/home/dev2/domains/siliconpin.s33.siliconpin.com/public_html/"
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ u2@siliconpin.com:~/web/siliconpin.com/public_html/"
},
"dependencies": {
"@astrojs/react": "^4.2.1",
@ -21,6 +21,7 @@
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"@uiw/react-md-editor": "^3.25.6",
"add": "^2.0.6",
"astro": "^5.5.2",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
@ -28,6 +29,7 @@
"date-fns": "^4.1.0",
"lucide-react": "^0.484.0",
"marked": "^15.0.8",
"menubar": "^9.5.1",
"minio": "^8.0.5",
"pocketbase": "^0.25.2",
"postcss": "^8.5.3",
@ -38,6 +40,7 @@
"react-simplemde-editor": "^5.2.0",
"react-to-print": "^3.0.5",
"rehype-rewrite": "^4.0.2",
"shadcn": "^2.5.0",
"simplemde": "^1.11.2",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^3.4.17",
@ -45,4 +48,4 @@
"xlsx": "^0.18.5"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>

After

Width:  |  Height:  |  Size: 437 B

BIN
public/assets/node-js.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.5.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.12-.07 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.963-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -3,10 +3,10 @@ import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../lib/InvoicePDF";
import { Eye, Download, ChevronUp, ChevronDown, Search } from "lucide-react";
import { Eye, Download, ChevronUp, ChevronDown, Search, FileText } from "lucide-react";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
import * as XLSX from 'xlsx';
export default function UserBillingList() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [billingData, setBillingData] = useState([]);
@ -14,30 +14,26 @@ export default function UserBillingList() {
const [apiError, setApiError] = useState(null);
const [selectedItem, setSelectedItem] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
// Sorting state
const [sortConfig, setSortConfig] = useState({ key: 'created_at', direction: 'desc' });
// Filtering state
const [filters, setFilters] = useState({
status: '',
cycle: '',
service: ''
});
const [filters, setFilters] = useState({ status: '', cycle: '', service: '' });
// Search state
const [searchTerm, setSearchTerm] = useState('');
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [itemsPerPage] = useState(20);
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
if (isLoggedIn) {
fetchBillingData();
}
}, [isLoggedIn]);
const fetchBillingData = async () => {
@ -52,12 +48,11 @@ export default function UserBillingList() {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
// console.log('Resoponse Data', data)
// For regular users, filter to only show their own billing data
const filteredData = sessionData?.user_type === 'admin'
? data.data || []
: (data.data || []).filter(item => item.user === sessionData?.email);
const filteredData = sessionData?.user_type === 'admin' || 'user' ? data.data || [] : (data.data || []).filter(item => item.user === sessionData?.email);
// console.log('Session Data', sessionData);
setBillingData(filteredData);
} catch (err) {
setApiError(err.message);
@ -65,7 +60,7 @@ export default function UserBillingList() {
setDataLoading(false);
}
};
// console.log('billing data', billingData)
const handleViewItem = (item) => {
setSelectedItem(item);
setDialogOpen(true);
@ -73,7 +68,7 @@ export default function UserBillingList() {
const closeModal = () => {
setDialogOpen(false);
setSelectedItem(null);
// setSelectedItem(null);
};
// Sorting functionality
@ -199,6 +194,24 @@ export default function UserBillingList() {
setSearchTerm(e.target.value);
setCurrentPage(1);
};
const exportToExcel = () => {
const worksheet = XLSX.utils.json_to_sheet(filteredAndSortedData.map(item => ({
'Billing ID': item.billing_id,
'Service': item.service,
'Customer Name': item.name,
'Customer Email': item.user,
'Silicon ID': item.siliconId,
'Amount': item.amount,
'cycle': item.cycle,
'Status': item.status,
'Created At': formatDate(item.created_at),
'Remarks': item.remarks
})));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Billing Data");
XLSX.writeFile(workbook, `billing_data_${new Date().toISOString().slice(0, 10)}.xlsx`);
};
const getStatusColor = (status) => {
switch (status) {
@ -228,11 +241,14 @@ export default function UserBillingList() {
if (!isLoggedIn) {
return <p className="text-center mt-8">You need to be logged in to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
// console.log('currentItems', currentItems)
return (
<section className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">My Billing History</h1>
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
<FileText className="w-4 h-4" /> Export to Excel
</Button>
</div>
{/* Results Count */}

View File

@ -0,0 +1,185 @@
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 { Loader2 } from "lucide-react";
export default function BuyVPN() {
// API URL - make sure to set this correctly
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
// State for VPN configuration
const [vpnContinent, setVpnContinent] = useState("");
const [selectedCycle, setSelectedCycle] = useState("");
const [selectedPrice, setSelectedPrice] = useState(0);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
// Price configuration
const PRICE_CONFIG = {
monthly: 100,
yearly: 1000
};
// Handle billing cycle selection
const handleBillingCycleChange = (cycle) => {
setSelectedCycle(cycle);
setSelectedPrice(cycle === 'monthly' ? PRICE_CONFIG.monthly : PRICE_CONFIG.yearly);
setError(null); // Clear any previous errors when user makes a selection
};
// Handle VPN purchase
const handlePurchase = async () => {
// Validate inputs
if (!vpnContinent) {
setError('Please select a continent');
return;
}
if (!selectedCycle) {
setError('Please select a billing cycle');
return;
}
setIsProcessing(true);
setError(null);
try {
const formData = new FormData();
formData.append('service', 'VPN WireGuard');
formData.append('serviceId', 'vpnservices');
formData.append('cycle', selectedCycle);
formData.append('amount', selectedPrice.toString());
formData.append('vpn_continent', vpnContinent);
formData.append('service_type', 'vpn');
const response = await fetch(`${USER_API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData, // Using FormData instead of JSON
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
// Redirect to payment success page with order ID
window.location.href = `/success?service=vpn&continent=${vpnContinent}&orderId=${data.order_id}`;
} else {
throw new Error(data.message || 'Payment initialization failed');
}
} catch (error) {
console.error('Purchase error:', error);
setError(error.message || 'An error occurred during purchase. Please try again.');
} finally {
setIsProcessing(false);
}
};
return (
<Card className="max-w-2xl mx-auto mt-8">
<CardHeader>
<CardTitle>VPN WireGuard</CardTitle>
<CardDescription>
Purchase your secure VPN subscription
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Continent Selection */}
<div>
<label htmlFor="vpn-continent" className="block text-sm font-medium mb-1">
Select Continent
</label>
<Select name="vpn-continent" onValueChange={setVpnContinent} value={vpnContinent} disabled={isProcessing}>
<SelectTrigger className="w-full">
<SelectValue placeholder="-Select-" />
</SelectTrigger>
<SelectContent>
<SelectItem value="india">India</SelectItem>
<SelectItem value="america">America</SelectItem>
<SelectItem value="europe">Europe</SelectItem>
</SelectContent>
</Select>
</div>
{/* VPN Features */}
<ul className="flex flex-wrap justify-between gap-2 text-xs">
{["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
{/* Billing Cycle Selection */}
{
vpnContinent && (
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedCycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input
type="radio"
name="vpn-cycle"
checked={selectedCycle === 'monthly'}
onChange={() => handleBillingCycleChange('monthly')}
className="hidden"
disabled={isProcessing}
/>
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.monthly}</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedCycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`}>
<input
type="radio"
name="vpn-cycle"
checked={selectedCycle === 'yearly'}
onChange={() => handleBillingCycleChange('yearly')}
className="hidden"
disabled={isProcessing}
/>
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.yearly}</p>
<span>Yearly</span>
</label>
</div>
)
}
{/* Selection Summary */}
{selectedCycle && (
<p className={`text-white ${selectedCycle === 'monthly' ? 'text-left' : 'text-end'}`}>
You selected <strong>{selectedCycle}</strong> plan at {selectedPrice}
</p>
)}
{/* Error Message */}
{error && (
<div className="p-3 bg-red-900/20 border border-red-700/50 rounded-md">
<p className="text-red-400">{error}</p>
</div>
)}
{/* Purchase Button */}
<Button
onClick={handlePurchase}
className={`w-full`}
disabled={!vpnContinent || !selectedCycle || isProcessing}
>
{isProcessing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
'Proceed'
)}
</Button>
</CardContent>
</Card>
);
}
// Cash Expense:- 950
// Card Expense:- 580

View File

@ -0,0 +1,214 @@
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 { Loader2 } from "lucide-react";
import { useToast } from "../ui/toast";
export default function NewDroplet() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: "",
region: "",
size: "",
image: "",
backups: false,
ipv6: true,
monitoring: false
});
const regions = [
{ value: "nyc1", label: "New York 1" },
{ value: "nyc3", label: "New York 3" },
{ value: "sfo3", label: "San Francisco 3" },
{ value: "ams3", label: "Amsterdam 3" },
];
const sizes = [
{ value: "s-1vcpu-1gb", label: "1 vCPU, 1GB RAM" },
{ value: "s-1vcpu-2gb", label: "1 vCPU, 2GB RAM" },
{ value: "s-2vcpu-2gb", label: "2 vCPU, 2GB RAM" },
];
const images = [
{ value: "ubuntu-22-04-x64", label: "Ubuntu 22.04" },
{ value: "ubuntu-20-04-x64", label: "Ubuntu 20.04" },
{ value: "debian-11-x64", label: "Debian 11" },
];
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSelectChange = (name, value) => {
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await fetch("https://api.digitalocean.com/v2/droplets", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf"
},
body: JSON.stringify({
...formData,
ssh_keys: [47441478]
})
});
const data = await response.json();
if (response.ok) {
showToast(`Droplet ${formData.name} created successfully!`, { type: 'success' });
// Reset form
setFormData({
name: "",
region: "nyc3",
size: "s-1vcpu-1gb",
image: "ubuntu-22-04-x64",
backups: false,
ipv6: true,
monitoring: false
});
} else {
throw new Error(data.message || "Failed to create droplet");
}
} catch (error) {
showToast(error.message, { type: 'error' });
} finally {
setIsLoading(false);
}
};
return (
<Card className="max-w-xl mx-auto px-4 my-4">
<CardHeader>
<CardTitle>Create New Droplet</CardTitle>
<CardDescription>Configure your new cloud server</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Droplet Name</Label>
<Input
id="name"
name="name"
type="text"
placeholder="my-droplet"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label>Region</Label>
<Select
value={formData.region}
onValueChange={(value) => handleSelectChange("region", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select region" />
</SelectTrigger>
<SelectContent>
{regions.map(region => (
<SelectItem key={region.value} value={region.value}>
{region.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Size</Label>
<Select
value={formData.size}
onValueChange={(value) => handleSelectChange("size", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select size" />
</SelectTrigger>
<SelectContent>
{sizes.map(size => (
<SelectItem key={size.value} value={size.value}>
{size.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Image</Label>
<Select
value={formData.image}
onValueChange={(value) => handleSelectChange("image", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select OS image" />
</SelectTrigger>
<SelectContent>
{images.map(image => (
<SelectItem key={image.value} value={image.value}>
{image.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="backups"
name="backups"
checked={formData.backups}
onChange={handleChange}
className="h-4 w-4"
/>
<Label htmlFor="backups">Enable Backups</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="ipv6"
name="ipv6"
checked={formData.ipv6}
onChange={handleChange}
className="h-4 w-4"
/>
<Label htmlFor="ipv6">Enable IPv6</Label>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Droplet"
)}
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,374 @@
import React, { useState, useEffect } 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 { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
export default function NewHetznerInstance() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [serverTypes, setServerTypes] = useState([]);
const [locations, setLocations] = useState([]);
const [images, setImages] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
const [formData, setFormData] = useState({
name: "",
server_type: "",
image: "",
location: "nbg1", // Default to Nuremberg
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch all required data in parallel
const [serverTypesResponse, locationsResponse, imagesResponse, sshKeysResponse] = await Promise.all([
fetch("https://api.hetzner.cloud/v1/server_types", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/locations", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/images", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
})
]);
const serverTypesData = await serverTypesResponse.json();
const locationsData = await locationsResponse.json();
const imagesData = await imagesResponse.json();
const sshKeysData = await sshKeysResponse.json();
setServerTypes(serverTypesData.server_types || []);
setLocations(locationsData.locations || []);
setImages(imagesData.images.filter(img => img.type === "system") || []);
setSshKeys(sshKeysData.ssh_keys || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
name: formData.name,
server_type: formData.server_type,
image: formData.image,
location: formData.location,
backups: formData.backups,
ssh_keys: formData.ssh_keys,
start_after_create: formData.start_after_create
};
// Add user_data if provided
if (formData.user_data) {
payload.user_data = formData.user_data;
}
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.server) {
showToast({
title: "Deployment Started",
description: `Server ${data.server.name} is being deployed`,
variant: "success"
});
// Reset form after successful deployment
setFormData({
name: "",
server_type: "",
image: "",
location: "nbg1",
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleAddSshKey = async () => {
try {
const response = await fetch('https://api.hetzner.cloud/v1/ssh_keys', {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
public_key: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.ssh_key) {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.ssh_keys || []);
showToast({
title: "SSH Key Added",
description: "Your SSH key has been successfully added",
variant: "success"
});
setShowAddSshKey(false);
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to add SSH key",
variant: "destructive"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Hetzner Cloud Server</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Server Name *</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="my-hetzner-server"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="location">Location *</Label>
<Select
name="location"
value={formData.location}
onValueChange={(value) => setFormData({...formData, location: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
{locations.map(location => (
<SelectItem key={location.name} value={location.name}>
{`${location.city} (${location.name.toUpperCase()})`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="server_type">Server Type *</Label>
<Select
name="server_type"
value={formData.server_type}
onValueChange={(value) => setFormData({...formData, server_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a server type" />
</SelectTrigger>
<SelectContent>
{serverTypes.map(type => (
<SelectItem key={type.name} value={type.name}>
{`${type.name} - ${type.cores} vCPU, ${type.memory}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{images.map(image => (
<SelectItem key={image.id} value={image.id.toString()}>
{image.description}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">SSH Keys</h3>
<div className="space-y-2">
<Label>Select SSH Keys</Label>
<div className="flex gap-2">
<Select
value={formData.ssh_keys[0] || ""}
onValueChange={(value) => setFormData({...formData, ssh_keys: [value]})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id.toString()}>{key.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => setShowAddSshKey(!showAddSshKey)}
>
{showAddSshKey ? "Cancel" : "Add New"}
</Button>
</div>
</div>
{showAddSshKey && (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="newSshKeyName">Key Name</Label>
<Input
id="newSshKeyName"
name="newSshKeyName"
value={formData.newSshKeyName}
onChange={handleChange}
placeholder="My Laptop Key"
/>
</div>
<div className="space-y-2">
<Label htmlFor="newSshKeyValue">Public Key</Label>
<Textarea
id="newSshKeyValue"
name="newSshKeyValue"
value={formData.newSshKeyValue}
onChange={handleChange}
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
rows={4}
/>
</div>
<Button
type="button"
onClick={handleAddSshKey}
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
>
Add SSH Key
</Button>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="user_data">Cloud-Init User Data (Optional)</Label>
<Textarea
id="user_data"
name="user_data"
value={formData.user_data}
onChange={handleChange}
placeholder="#cloud-config\nwrite_files:\n - content: |\n Hello World\n path: /tmp/hello.txt"
rows={6}
/>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="backups"
checked={formData.backups}
onCheckedChange={(checked) => setFormData({...formData, backups: checked})}
/>
<Label htmlFor="backups">Enable Backups (+20% cost)</Label>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying..." : "Deploy Server"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,386 @@
import React, { useState, useEffect } 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 { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { Switch } from "../ui/switch";
export default function NewCloudInstance() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
const [formData, setFormData] = useState({
dcslug: "inmumbaizone2",
planid: "",
hostname: "",
image: "debian-12-x86_64",
auth: "ssh_key",
sshkeys: "",
password: "",
enable_publicip: true,
enablebackup: false,
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch plans and SSH keys in parallel
const [plansResponse, sshResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: { "Authorization": UTHO_API_KEY }
}),
fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": UTHO_API_KEY }
})
]);
const plansData = await plansResponse.json();
const sshData = await sshResponse.json();
setPlans(plansData.plans || []);
setSshKeys(sshData.key || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchData();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
const payload = {
dcslug: formData.dcslug,
planid: formData.planid,
hostname: formData.hostname,
image: formData.image,
enable_publicip: formData.enable_publicip ? "true" : "false",
enablebackup: formData.enablebackup ? "true" : "false",
cloud: [{ hostname: formData.hostname }],
vpc: "bcb68d3d-60f5-495f-9513-6b6a4e53a470",
billingcycle: "hourly"
};
// Add authentication based on selected method
if (formData.auth === "ssh_key") {
payload.sshkeys = formData.sshkeys;
} else {
payload.password = formData.password;
}
const response = await fetch("https://api.utho.com/v2/cloud/deploy", {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.status === "success") {
showToast({
title: "Deployment Started",
description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
variant: "success"
});
// Reset form after successful deployment
setFormData(prev => ({
...prev,
hostname: "",
planid: ""
}));
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleAddSshKey = async () => {
try {
const response = await fetch('https://api.utho.com/v2/key/import', {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
sshkey: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.status === 'success') {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.utho.com/v2/key", {
headers: { "Authorization": UTHO_API_KEY }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.key || []);
showToast({
title: "SSH Key Added",
description: "Your SSH key has been successfully added",
variant: "success"
});
setShowAddSshKey(false);
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to add SSH key",
variant: "destructive"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Cloud Instance</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="hostname">Hostname *</Label>
<Input
id="hostname"
name="hostname"
value={formData.hostname}
onChange={handleChange}
placeholder="server1.example.com"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="planid">Plan *</Label>
<Select
name="planid"
value={formData.planid}
onValueChange={(value) => setFormData({...formData, planid: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{/* {plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
</SelectItem>
))} */}
<SelectItem key={plans[0].id} value={plans[0].id}>
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="debian-12-x86_64">Debian 12</SelectItem>
<SelectItem value="ubuntu-22.04">Ubuntu 22.04</SelectItem>
<SelectItem value="centos-7.4-x86_64">CentOS 7.4</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Authentication</h3>
<Tabs
value={formData.auth}
onValueChange={(value) => setFormData({...formData, auth: value})}
className="w-full">
<TabsList className="grid w-full grid-cols-2 gap-4">
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'ssh_key' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="ssh_key">SSH Key</TabsTrigger>
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'password' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="ssh_key" className="space-y-4">
<div className="space-y-2">
<Label>Select SSH Key</Label>
<div className="flex gap-2">
<Select
value={formData.sshkeys}
onValueChange={(value) => setFormData({...formData, sshkeys: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id}>{key.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="outline"
className="whitespace-nowrap"
onClick={() => setShowAddSshKey(!showAddSshKey)}
>
{showAddSshKey ? "Cancel" : "Add New"}
</Button>
</div>
</div>
{showAddSshKey && (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="newSshKeyName">Key Name</Label>
<Input
id="newSshKeyName"
name="newSshKeyName"
value={formData.newSshKeyName}
onChange={handleChange}
placeholder="My Laptop Key"
/>
</div>
<div className="space-y-2">
<Label htmlFor="newSshKeyValue">Public Key</Label>
<Textarea
id="newSshKeyValue"
name="newSshKeyValue"
value={formData.newSshKeyValue}
onChange={handleChange}
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
rows={4}
/>
</div>
<Button
type="button"
onClick={handleAddSshKey}
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
>
Add SSH Key
</Button>
</div>
)}
</TabsContent>
<TabsContent value="password" className="space-y-2">
<div className="space-y-2">
<Label htmlFor="password">Root Password *</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter root password"
required={formData.auth === "password"}
/>
</div>
</TabsContent>
</Tabs>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="enable_publicip"
checked={formData.enable_publicip}
onCheckedChange={(checked) => setFormData({...formData, enable_publicip: checked})}
/>
<Label htmlFor="enable_publicip">Enable Public IP</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="enablebackup"
checked={formData.enablebackup}
onCheckedChange={(checked) => setFormData({...formData, enablebackup: checked})}
/>
<Label htmlFor="enablebackup">Enable Weekly Backups (+20%)</Label>
</div>
</div>
</div>
<div className="flex justify-end pt-4">
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
{isLoading ? "Deploying..." : "Deploy Instance"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Input } from './ui/input';
import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
import { Select } from './ui/select';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
import { Button } from './ui/button';
export function ContactForm() {
@ -16,22 +16,28 @@ export function ContactForm() {
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormState(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
const handleSelectChange = (value: string) => {
setFormState(prev => ({ ...prev, service: value }));
console.log(formState)
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormStatus('submitting');
// Simulate form submission with a timeout
setTimeout(() => {
// In a real app, you would send the data to a server here
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('Form submitted:', formState);
setFormStatus('success');
// Reset form after successful submission
// Reset form
setFormState({
name: '',
email: '',
@ -40,64 +46,103 @@ export function ContactForm() {
message: '',
});
// Reset status after showing success message for a while
setTimeout(() => {
setFormStatus('idle');
}, 3000);
}, 1500);
setTimeout(() => setFormStatus('idle'), 3000);
} catch (error) {
console.error('Submission error:', error);
setFormStatus('error');
setTimeout(() => setFormStatus('idle'), 3000);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
<div className="space-y-3 sm:space-y-4">
{/* Name and Email - Stack on mobile, side-by-side on tablet+ */}
{/* Name and Email */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="name" className="text-sm sm:text-base">Name</Label>
<Input id="name" name="name" placeholder="Your name" value={formState.name} onChange={handleChange} required className="text-sm sm:text-base" />
<Label htmlFor="name" className="text-sm sm:text-base">Name*</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formState.name}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="email" className="text-sm sm:text-base">Email</Label>
<Input id="email" name="email" type="email" placeholder="your.email@example.com" value={formState.email} onChange={handleChange} required className="text-sm sm:text-base" />
<Label htmlFor="email" className="text-sm sm:text-base">Email*</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your.email@example.com"
value={formState.email}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
</div>
{/* Company and Service Interest - Stack on mobile, side-by-side on tablet+ */}
{/* Company and Service Interest */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
<Input id="company" name="company" placeholder="Your company name" value={formState.company} onChange={handleChange} className="text-sm sm:text-base" />
<Input
id="company"
name="company"
placeholder="Your company name"
value={formState.company}
onChange={handleInputChange}
disabled={formStatus === 'submitting'}
className="text-sm sm:text-base"
/>
</div>
{/* dop_v1_b6a075ece5786faf7c58d21761dbf95d47af372da062d68a870ce2a0bae51adf */}
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest</Label>
<Select id="service" name="service" value={formState.service} onChange={handleChange} required className="text-sm sm:text-base">
<option value="" disabled>Select a service</option>
<option value="php">PHP Hosting</option>
<option value="nodejs">Node.js Hosting</option>
<option value="python">Python Hosting</option>
<option value="kubernetes">Kubernetes (K8s)</option>
<option value="k3s">K3s Lightweight Kubernetes</option>
<option value="custom">Custom Solution</option>
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest*</Label>
<Select
value={formState.service}
onValueChange={handleSelectChange}
disabled={formStatus === 'submitting'}
>
<SelectTrigger className="text-sm sm:text-base">
<SelectValue placeholder="Select a service" /></SelectTrigger>
<SelectContent>
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
<SelectItem value="python">Python Hosting</SelectItem>
<SelectItem value="kubernetes">Kubernetes (K8s)</SelectItem>
<SelectItem value="k3s">K3s Lightweight Kubernetes</SelectItem>
<SelectItem value="custom">Custom Solution</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Message */}
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="message" className="text-sm sm:text-base">Message</Label>
<Label htmlFor="message" className="text-sm sm:text-base">Message*</Label>
<Textarea
id="message"
name="message"
placeholder="Tell us about your project requirements..."
value={formState.message}
onChange={handleChange}
onChange={handleInputChange}
required
disabled={formStatus === 'submitting'}
className="min-h-[100px] sm:min-h-[150px] text-sm sm:text-base"
/>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
size="lg"
@ -115,17 +160,24 @@ export function ContactForm() {
) : 'Send Message'}
</Button>
{/* Status Messages */}
{formStatus === 'success' && (
<div className="p-3 sm:p-4 bg-green-900/30 border border-green-800 rounded-md text-green-400 text-center text-sm sm:text-base">
<div
className="p-3 sm:p-4 bg-green-900/30 border border-green-800 rounded-md text-green-400 text-center text-sm sm:text-base"
aria-live="polite"
>
Thank you for your message! We'll get back to you soon.
</div>
)}
{formStatus === 'error' && (
<div className="p-3 sm:p-4 bg-red-900/30 border border-red-800 rounded-md text-red-400 text-center text-sm sm:text-base">
<div
className="p-3 sm:p-4 bg-red-900/30 border border-red-800 rounded-md text-red-400 text-center text-sm sm:text-base"
aria-live="assertive"
>
There was an error sending your message. Please try again.
</div>
)}
</form>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,963 @@
import React, { useState, useEffect } from 'react';
import { Toast } from './Toast';
import { TemplatePreview } from './TemplatePreview';
import { Button } from './ui/button';
import { Input } from "./ui/input";
import { Label, Select } from '@radix-ui/react-select';
export const DomainSetupForm = ({ defaultSubdomain }) => {
// Deployment type and app selections
const [deploymentType, setDeploymentType] = useState('app');
const [appType, setAppType] = useState('wordpress');
const [sampleWebAppType, setSampleWebAppType] = useState('developer'); // New state for sample web app type
const [sourceType, setSourceType] = useState('public');
const [repoUrl, setRepoUrl] = useState('');
const [deploymentKey, setDeploymentKey] = useState('');
const [fileName, setFileName] = useState('');
// Domain configuration
const [useSubdomain, setUseSubdomain] = useState(true);
const [useCustomDomain, setUseCustomDomain] = useState(false);
const [customDomain, setCustomDomain] = useState('');
const [customSubdomain, setCustomSubdomain] = useState('');
const [domainType, setDomainType] = useState('domain'); // 'domain' or 'subdomain'
// Domain validation states
const [isValidating, setIsValidating] = useState(false);
const [isValidDomain, setIsValidDomain] = useState(false);
const [validationMessage, setValidationMessage] = useState('');
// DNS configuration
const [dnsMethod, setDnsMethod] = useState('cname');
const [showDnsConfig, setShowDnsConfig] = useState(false);
const [dnsVerified, setDnsVerified] = useState({ cname: false, ns: false, a: false, ip: false });
const [txnId, setTxnId] = useState('');
const [userEmail, setUserEmail] = useState('');
const [panelType, setPanelType] = useState('');
const [panelServicesId, setPanelServicesId] = useState('');
const [vpnContinent, setVpnContinent] = useState('');
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
// const BILLING_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
const [selectedcycle, setSelectedcycle] = useState('');
const [selectedPrice, setSelectedPrice] = useState(0);
const handleCheckboxChange = (cycle, price) => {
if (selectedcycle === cycle) {
setSelectedcycle('');
setSelectedPrice(0);
} else {
setSelectedcycle(cycle);
setSelectedPrice(price);
}
// console.log(selectedcycle, ' ', selectedPrice);
};
const handlePanelBuyNow = (whichService) => {
// console.log(whichService);
// Disable button during processing
const buyButton = document.getElementById('buy-button'); // Add ID to your button
if (buyButton) buyButton.disabled = true;
showToast('Loading...');
if(whichService === 'php-mysql-with-admin-panel'){
// console.log('php-mysql-with-admin-panel: ', whichService);
const formData = new FormData();
formData.append('service', panelType);
formData.append('serviceId', panelServicesId);
formData.append('cycle', selectedcycle);
formData.append('amount', selectedPrice); //selectedPrice
fetch(`${API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData,
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if(data.success === true){
setTxnId(data.txn_id);
setUserEmail(data.user_email);
window.location.href = `/make-payment?query=get-initiated_payment&orderId=${data.order_id}`
// redirectToPayU(data);
showToast('Redirecting to payment page...');
} else {
throw new Error(data.message || 'Payment initialization failed');
}
})
.catch(error => {
showToast(error.message || 'Payment failed. Please try again.');
console.error('An error occurred:', error);
})
.finally(() => {
if (buyButton) buyButton.disabled = false;
});
} else if(whichService === 'vpn'){
const formData = new FormData();
formData.append('service', deploymentType);
formData.append('serviceId', 'vpnservices');
formData.append('cycle', selectedcycle);
formData.append('amount', selectedPrice);
fetch(`${API_URL}?query=initiate_payment`, {
method: 'POST',
body: formData,
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if(data.success === true){
setTxnId(data.txn_id);
setUserEmail(data.user_email);
// window.location.href = `/make-payment?query=get-initiated_payment&orderId=${data.order_id}`
window.location.href = `/success?service=vpn&orderId=${data.order_id}`
// redirectToPayU(data);
showToast('Redirecting to payment page...');
} else {
throw new Error(data.message || 'Payment initialization failed');
}
})
.catch(error => {
showToast(error.message || 'Payment failed. Please try again.');
console.error('An error occurred:', error);
})
.finally(() => {
if (buyButton) buyButton.disabled = false;
});
}
};
// Form validation
const [formValid, setFormValid] = useState(true);
// Toast notification
const [toast, setToast] = useState({ visible: false, message: '' });
// File upload reference
const fileInputRef = React.useRef(null);
// Effect for handling domain type changes
useEffect(() => {
if (!useCustomDomain) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
}
validateForm();
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, dnsVerified.ns, domainType, dnsMethod]);
// Show toast notification
const showToast = (message) => {
setToast({ visible: true, message });
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
};
// Handle deployment type change
const handleDeploymentTypeChange = (e) => {
setDeploymentType(e.target.value);
};
// Handle source type change
const handleSourceTypeChange = (e) => {
setSourceType(e.target.value);
};
// Handle file upload
const handleFileChange = (e) => {
if (e.target.files.length > 0) {
setFileName(e.target.files[0].name);
} else {
setFileName('');
}
};
// Handle domain checkbox changes
const handleUseSubdomainChange = (e) => {
setUseSubdomain(e.target.checked);
// If CNAME record is selected, SiliconPin subdomain must be enabled
if (useCustomDomain && dnsMethod === 'cname' && !e.target.checked) {
setUseCustomDomain(false);
}
validateForm();
};
const handleUseCustomDomainChange = (e) => {
setUseCustomDomain(e.target.checked);
if (!e.target.checked) {
setShowDnsConfig(false);
setIsValidDomain(false);
setValidationMessage('');
setIsValidating(false);
setDnsVerified({
cname: false,
ns: false,
a: false,
ip: false
});
} else {
// Force SiliconPin subdomain to be checked if custom domain is checked
setUseSubdomain(true);
}
validateForm();
};
// Handle domain type change
const handleDomainTypeChange = (e) => {
setDomainType(e.target.value);
// Reset validation when changing domain type
setIsValidDomain(false);
setValidationMessage('');
setDnsVerified({
cname: false,
ns: false,
a: false
});
validateForm();
};
// Handle domain and subdomain input changes
const handleDomainChange = (e) => {
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?(www\.)?/i, '').replace(/\/+$/, '').trim();
setCustomDomain(cleanedValue);
};
const handleSubdomainChange = (e) => {
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?/i, '').replace(/\/+$/, '').trim();
setCustomSubdomain(cleanedValue);
};
// Handle DNS method change
const handleDnsMethodChange = (e) => {
setDnsMethod(e.target.value);
// If changing to CNAME, ensure SiliconPin subdomain is enabled
if (e.target.value === 'cname') {
setUseSubdomain(true);
}
// Reset DNS verification
setDnsVerified(prev => ({
...prev,
[e.target.value]: false
}));
validateForm();
};
// Validate domain
const validateDomain = async () => {
const domain = domainType === 'domain' ? customDomain : customSubdomain;
// Reset validation state
setIsValidating(true);
setIsValidDomain(false);
setValidationMessage('');
setShowDnsConfig(false);
// Check if domain is empty
if (!domain) {
setValidationMessage('Please enter a domain name.');
setIsValidating(false);
setIsValidDomain(false);
return;
}
// Validate domain format
const domainFormatRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
if (!domainFormatRegex.test(domain)) {
setValidationMessage('Domain format is invalid. Please check your entry.');
setIsValidating(false);
setIsValidDomain(false);
return;
}
try {
// Make API call to validate domain
const response = await fetch(`${SERVICES_API_URL}?query=validate-domain`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ domain })
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
if (data.valid) {
setValidationMessage('Domain is valid and registered.');
setIsValidDomain(true);
setShowDnsConfig(true);
} else {
setValidationMessage(data.message || 'Domain appears to be unregistered or unavailable.');
setIsValidDomain(false);
}
} catch (error) {
console.error('Domain validation error:', error);
setValidationMessage('Error validating domain. Please try again.');
setIsValidDomain(false);
} finally {
setIsValidating(false);
validateForm();
}
};
// Check DNS configuration
const checkSubDomainCname = () => {
const domainToCheck = customDomain || customSubdomain;
fetch(`${SERVICES_API_URL}?query=check-c-name`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `domain=${encodeURIComponent(domainToCheck)}`
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.status === 'success') {
checkDnsConfig('cname');
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
// console.log('CNAME record:', data.cname);
// Handle success - update UI
} else {
console.error('Error:', data.message);
// Handle error
}
})
.catch(error => {
console.error('Fetch error:', error);
// Show error to user
});
};
const checkDnsConfig = (type) => {
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
// Simulate DNS check
setTimeout(() => {
setDnsVerified(prev => ({
...prev,
[type]: true
}));
showToast(`${type} verified successfully!`);
validateForm();
}, 1500);
};
// Copy to clipboard
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
showToast('Copied to clipboard!');
})
.catch(err => {
showToast('Failed to copy: ' + err);
});
};
// Validate form
const validateForm = () => {
// For custom domain, require DNS verification
if (useCustomDomain) {
if (dnsMethod === 'cname' && !dnsVerified.cname) {
setFormValid(false);
return;
}
if (dnsMethod === 'ns' && !dnsVerified.ns) {
setFormValid(false);
return;
}
if (dnsMethod === 'ip' && !dnsVerified.ip) {
setFormValid(false);
return;
}
}
setFormValid(true);
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (!formValid) {
showToast('Please complete DNS verification before deploying.');
return;
}
// In a real app, this would submit the form data to the server
// console.log([{ deploymentType, appType, sampleWebAppType, sourceType, repoUrl, deploymentKey, useSubdomain, useCustomDomain, customDomain, customSubdomain, domainType, dnsMethod}]);
showToast('Form submitted successfully!');
};
return (
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Deployment Type Selection */}
<div className="space-y-2">
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
<select
id="deployment-type"
value={deploymentType}
onChange={handleDeploymentTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="app">💻 Deploy an App</option>
<option value="source"> From Source</option>
<option value="static">📄 Static Site Upload</option>
<option value="sample-web-app">🌐 Sample Web App</option>
<option value="php-mysql-with-admin-panel">🗄 PHP MYSQL with a admin panel</option>
<option value="vpn">🔒 VPN</option>
</select>
</div>
{/* App Options */}
{deploymentType === 'app' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Application</label>
<select
id="app-type"
value={appType}
onChange={(e) => setAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="wordpress">🔌 WordPress</option>
<option value="prestashop">🛒 PrestaShop</option>
<option value="laravel">🚀 Laravel</option>
<option value="cakephp">🍰 CakePHP</option>
<option value="symfony">🎯 Symfony</option>
</select>
</div>
)}
{/* Sample Web App Options */}
{deploymentType === 'sample-web-app' && (
<div className="space-y-2">
<label htmlFor="sample-web-app-type" className="block text-white font-medium">Select Template Type</label>
<select
id="sample-web-app-type"
value={sampleWebAppType}
onChange={(e) => setSampleWebAppType(e.target.value)}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="developer">👨💻 Developer Portfolio</option>
<option value="designer">🎨 Designer Portfolio</option>
<option value="photographer">📸 Photographer Portfolio</option>
<option value="documentation">📚 Documentation Site</option>
<option value="business">🏢 Single Page Business Site</option>
</select>
<div className="mt-4 p-4 bg-neutral-700/30 rounded-md border border-neutral-600">
<div className="flex items-start">
<div className="mr-3 text-2xl"></div>
<div className="flex-1">
<p className="text-sm text-neutral-300">
Deploy a ready-to-use template that you can customize. We'll set up the basic structure and you can modify it to fit your needs.
</p>
</div>
</div>
</div>
{/* Template Preview */}
<TemplatePreview templateType={sampleWebAppType} />
</div>
)}
{/* Source Options */}
{deploymentType === 'source' && (
<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="source-type" className="block text-white font-medium">Source Type</label>
<select
id="source-type"
value={sourceType}
onChange={handleSourceTypeChange}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="public">🌐 Public Repository</option>
<option value="private">🔒 Private Repository</option>
</select>
</div>
<div className="pt-2">
<label htmlFor="repo-url" className="block text-white font-medium mb-2">Repository URL</label>
<input
type="text"
id="repo-url"
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
placeholder="https://github.com/username/repository"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
</div>
{sourceType === 'private' && (
<div className="pt-2">
<div className="flex items-center gap-2 mb-2">
<label htmlFor="deployment-key" className="block text-white font-medium">Deployment Key</label>
<button type="button" onClick={() => showToast('Deployment keys are used for secure access to private repositories')} className="text-neutral-400 hover:text-white focus:outline-none" aria-label="Deployment Key Information">
<span className="text-lg"></span>
</button>
</div>
<textarea id="deployment-key" value={deploymentKey} onChange={(e) => setDeploymentKey(e.target.value)} placeholder="Paste your SSH private key here" rows="6" className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37] font-mono text-sm" />
<p className="mt-1 text-sm text-neutral-400">Your private key is used only for deploying and is never stored on our servers.</p>
</div>
)}
</div>
)}
{/* Static Site Options */}
{deploymentType === 'static' && (
<div className="space-y-4">
<div className="p-4 bg-neutral-700/50 rounded-md text-neutral-300 text-center">
Upload the zip file containing your static website
</div>
<div className="pt-2">
<label htmlFor="file-upload" className="block text-white font-medium mb-2">Upload File (ZIP/TAR)</label>
<div className="flex items-center justify-center w-full">
<label
htmlFor="file-upload"
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
>
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<span className="text-3xl mb-3 text-neutral-400">📁</span>
<p className="mb-2 text-sm text-neutral-400">
<span className="font-semibold">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-neutral-500">ZIP or TAR files only (max. 100MB)</p>
</div>
<input
id="file-upload"
ref={fileInputRef}
type="file"
onChange={handleFileChange}
className="hidden"
accept=".zip,.tar"
/>
</label>
</div>
{fileName && (
<div className="mt-2 text-sm text-neutral-400">
Selected file: {fileName}
</div>
)}
</div>
</div>
)}
{
deploymentType === 'php-mysql-with-admin-panel' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Panel</label>
<select
id="app-type"
value={panelType}
onChange={(e) => {
setPanelType(e.target.value);
setPanelServicesId(e.target.options[e.target.selectedIndex].dataset.id);
}}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="">-Select-</option>
<option value="Hestia-Panel" data-id="4ksYhjFy">🧰 Hestia Panel</option>
<option value="Webmin" data-id="5ksYAhFz">🖥 Webmin</option>
<option value="cPanel" data-id="6jgThjZx">📊 cPanel</option>
</select>
</div>
)
}
{
deploymentType === 'vpn' && (
<div className="space-y-2">
<label htmlFor="app-type" className="block text-white font-medium">Select Contient</label>
<select
id="app-type"
value={vpnContinent}
onChange={(e) => {
setVpnContinent(e.target.value);
// setVpnContinent(e.target.options[e.target.selectedIndex].dataset.id);
}}
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
>
<option value="">-Select-</option>
<option value="India" data-id="7ksXhkFy">India</option>
<option value="America" data-id="8ksAshGz">America</option>
<option value="Europe" data-id="9LgFhjVx">Europe</option>
</select>
</div>
)
}
{
deploymentType === 'vpn' && (
<>
<ul className="flex justify-between text-sm">
{["Unlimited bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedcycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'monthly'} onChange={() => handleCheckboxChange('monthly', 100)} className="hidden" />
<p className='text-3xl font-bold text-center'>100</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedcycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'yearly'} onChange={() => handleCheckboxChange('yearly', 1000)} className="hidden" />
<p className='text-3xl font-bold text-center'>1000</p>
<span>Yearly</span>
</label>
</div>
<div className='text-white'>
{selectedcycle && (
<p className={`${selectedcycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedcycle}</strong> plan at {selectedPrice}</p>
)}
</div>
</>
)
}
{
deploymentType === 'php-mysql-with-admin-panel' && panelType === 'Hestia-Panel' && (
<>
<ul className="flex justify-between text-sm">
{["5 Domains", "free Let's Encrypt SSL", "1 MariaDB database", "phpMyAdmin"].map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" ><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-400'>{feature}</span>
</li>
))}
</ul>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label className={`border ${selectedcycle === 'monthly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'monthly'} onChange={() => handleCheckboxChange('monthly', 200)} className="hidden" />
<p className='text-3xl font-bold text-center'>200</p>
<span>Monthly</span>
</label>
<label className={`border ${selectedcycle === 'yearly' ? 'bg-[#6d9e37]' : ''} border-[#6d9e37] text-white px-4 py-2 text-center rounded-md w-full cursor-pointer`}>
<input type="checkbox" checked={selectedcycle === 'yearly'} onChange={() => handleCheckboxChange('yearly', 2000)} className="hidden" />
<p className='text-3xl font-bold text-center'>2000</p>
<span>Yearly</span>
</label>
</div>
<div className='text-white'>
{selectedcycle && (
<p className={`${selectedcycle === 'monthly' ? 'text-left' : 'text-end'}`}>You selected <strong>{selectedcycle}</strong> plan at {selectedPrice}</p>
)}
</div>
</>
)
}
{
deploymentType === 'php-mysql-with-admin-panel' && panelType === 'cPanel' ? (
<div>
<p>
cPanel is a proprietary software. Here at Siliconpin, we encourage using freedom-oriented software.
If you need a cPanel, you can visit &nbsp;
<a className='text-[#6d9e37]' href="https://cicdhosting.com" target='_blank'>https://cicdhosting.com</a>
</p>
</div>
) : deploymentType === 'php-mysql-with-admin-panel' && (
<Button onClick={() => {handlePanelBuyNow('php-mysql-with-admin-panel')}} className='w-full'>Proceed to Pay</Button>
)
}
{
deploymentType === 'vpn' && (
<Button onClick={() => {handlePanelBuyNow('vpn')}} className='w-full'>Proceed to Pay</Button>
)
}
{
deploymentType !== 'php-mysql-with-admin-panel' && deploymentType !== 'vpn' &&(
<>
{/* Domain Configuration */}
<div className="pt-4 border-t border-neutral-700">
<h3 className="text-lg font-medium text-white mb-4">Destination</h3>
<div className="space-y-4">
{/* SiliconPin Subdomain */}
<div className="flex items-start gap-2">
<input type="checkbox" id="use-subdomain" checked={useSubdomain} onChange={handleUseSubdomainChange} className="mt-1 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="use-subdomain" className="block text-white font-medium">Use SiliconPin Subdomain</label>
<div className="mt-2 flex">
<input
type="text"
id="subdomain"
name="destination-domain"
value={defaultSubdomain}
className="rounded-l-md py-2 px-3 bg-neutral-600 border-y border-l border-neutral-600 text-neutral-300 focus:outline-none w-1/3 font-mono"
readOnly
/>
<span className="rounded-r-md py-2 px-3 bg-neutral-800 border border-neutral-700 text-neutral-400 w-2/3">.subdomain.siliconpin.com</span>
</div>
</div>
</div>
{/* Custom Domain */}
<div className="flex items-start gap-2">
<input type="checkbox" id="use-custom-domain" name="destination-domain" checked={useCustomDomain} onChange={handleUseCustomDomainChange} className="mt-1 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
{useCustomDomain && (
<div className="mt-3 space-y-4">
{/* Domain Type Selection */}
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
<div className="flex items-center">
<input type="radio" id="domain-type-domain" name="domain-type" value="domain" checked={domainType === 'domain'} onChange={handleDomainTypeChange} className="mr-2 accent-[#6d9e37]" />
<label htmlFor="domain-type-domain" className="text-white">Root Domain</label>
</div>
<div className="flex items-center">
<input type="radio" id="domain-type-subdomain" name="domain-type" value="subdomain" checked={domainType === 'subdomain'} onChange={handleDomainTypeChange} className="mr-2 accent-[#6d9e37]"/>
<label htmlFor="domain-type-subdomain" className="text-white">Subdomain</label>
</div>
</div>
{/* Domain Input */}
{domainType === 'domain' ? (
<div>
<input
type="text"
id="custom-domain"
value={customDomain}
onChange={handleDomainChange}
placeholder="yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter domain without http://, www, or trailing slashes (example.com). You can configure www or other subdomains later.
</p>
</div>
) : (
<div>
<input
type="text"
id="custom-subdomain"
value={customSubdomain}
onChange={handleSubdomainChange}
placeholder="blog.yourdomain.com"
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
/>
<p className="mt-1 text-xs text-neutral-400">
Enter the full subdomain without http:// or trailing slashes. www and protocol prefixes will be automatically removed.
</p>
</div>
)}
{/* Domain Validation */}
<button
disabled={!customDomain && !customSubdomain}
type="button"
onClick={validateDomain}
className={`px-4 py-2 ${ !customDomain && !customSubdomain ? 'bg-neutral-600 cursor-not-allowed' : 'bg-[#6d9e37] focus:ring-[#6d9e37] transition-colors'} text-white font-medium rounded-md transition-colors focus:outline-none`}
>
Validate Domain
</button>
{/* Validation Status */}
{useCustomDomain && isValidating && (
<div className="p-3 bg-neutral-700/50 rounded-md">
<div className="flex items-center justify-center space-x-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
<p className="text-white">Verifying domain registration and availability...</p>
</div>
</div>
)}
{useCustomDomain && !isValidating && validationMessage && (
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
{validationMessage}
</p>
</div>
)}
{/* DNS Configuration Options */}
{showDnsConfig && (
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
<h4 className="text-white font-medium">Connect Your Domain</h4>
{/* CNAME Record Option */}
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'cname' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-cname" className={`flex items-start cursor-pointer`}>
<input type="radio" id="dns-cname" name="dns-method" value="cname" checked={dnsMethod === 'cname'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
<div className="mt-3 flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
{defaultSubdomain}.subdomain.siliconpin.com
</div>
<button
type="button"
onClick={() => copyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
className="ml-2 text-[#6d9e37] hover:text-white"
aria-label="Copy CNAME value"
>
<span className="text-lg">📋</span>
</button>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => (checkSubDomainCname())}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.cname
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.cname ? '✓ CNAME Verified' : 'Check CNAME'}
</button>
</div>
</div>
</label>
</div>
{/* Nameserver Option (only for full domains, not subdomains) */}
{domainType === 'domain' && (
<>
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'ns' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-ns" className="flex items-start cursor-pointer">
<input type="radio" id="dns-ns" name="dns-method" value="ns" checked={dnsMethod === 'ns'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
<div className="mt-3 space-y-2">
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
<button type="button" onClick={() => copyToClipboard('ns1.siliconpin.com')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
<button type="button" onClick={() => copyToClipboard('ns2.siliconpin.com')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('ns')}
className={`px-3 py-1 text-white text-sm rounded
${dnsVerified.ns
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
</button>
</div>
</div>
</label>
</div>
<div className={`p-4 bg-neutral-700/30 rounded-md space-y-3 ${dnsMethod === 'ip' ? 'border-2 border-[#6d9e37]' : 'border border-neutral-600'}`}>
<label for="dns-ip" className="flex items-start cursor-pointer">
<input type="radio" id="dns-ip" name="dns-method" value="ip" checked={dnsMethod === 'ip'} onChange={handleDnsMethodChange} className="mt-1 mr-2 accent-[#6d9e37]" />
<div className="flex-1">
<label htmlFor="dns-ip" className="block text-white font-medium">Use Our IP Address</label>
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
<div className="mt-3 space-y-2">
<div className="flex items-center">
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">xxx.xxx.x.xx</div>
<button type="button" onClick={() => copyToClipboard('xxx.xxx.x.xx')} className="ml-2 text-[#6d9e37] hover:text-white" aria-label="Copy nameserver value">
<span className="text-lg">📋</span>
</button>
</div>
</div>
<div className="mt-2 text-right">
<button
type="button"
onClick={() => checkDnsConfig('ip')}
className={`px-3 py-1 text-white text-sm rounded
${dnsMethod === 'ip'
? 'bg-green-700 hover:bg-green-600'
: 'bg-neutral-600 hover:bg-neutral-500'}`}
>
{/* {dnsVerified.ip ? '✓ IP Address Verified' : 'Check IP Address'} */}
Proceed to Pay
</button>
</div>
</div>
</label>
</div>
</>
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
{/* Form Submit Button */}
<Button
type="submit"
disabled={useCustomDomain && !formValid}
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
${useCustomDomain && !formValid
? 'bg-neutral-600 cursor-not-allowed'
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
}`}
>
Start the Deployment
</Button>
</>
)
}
</form>
<Toast visible={toast.visible} message={toast.message} />
</div>
);
};
// upi://pay?pa=merchant@bank&pn=Merchant%20Inc&am=100.00&cu=INR&tn=Payment%20for%20goods

View File

@ -27,8 +27,6 @@ interface AuthResponse {
token: string;
record: UserRecord;
}
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@ -125,12 +123,42 @@ const LoginPage = () => {
}
};
const handleCreateUserInMongo = async (siliconId: string) => {
try {
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
siliconId: siliconId
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('MongoDB Response:', data);
return data;
} catch (error) {
console.error('MongoDB Creation Error:', error);
throw error; // Return Error
}
};
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try {
// Step 1: Sync with SiliconPin backend
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
@ -149,10 +177,17 @@ const LoginPage = () => {
}
const data = await response.json();
console.log('Session synced with backend:', data);
console.log('Backend Sync Data:', data);
// Step 2: creating user in MongoDB
await handleCreateUserInMongo(data.userData.userId);
// Step 3: Redirect to profile if all are ok
window.location.href = '/profile';
return data;
} catch (error: any) {
console.error('Error syncing session:', error);
console.error('Sync Error:', error);
throw new Error(`Session sync failed: ${error.message}`);
}
};
@ -163,6 +198,7 @@ const LoginPage = () => {
}
if(isLoggedIn){
return (
// <></>
window.location.href = '/'
)
}

View File

@ -0,0 +1,63 @@
import React from "react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
export default function LoginOrProfile({ deviceType }) {
const { isLoggedIn, loading } = useIsLoggedIn();
if (loading) {
return (
<>
{
deviceType === 'desktop' && (
<p className="w-6 animate-pulse hover:text-[#6d9e37] transition-colors"></p>
)
}
{
deviceType === 'mobile' && (
<svg className="animate-spin flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g id="Download_x5F_25_x25_"> </g> <g id="Download_x5F_50_x25_"> </g> <g id="Download_x5F_75_x25_"> </g> <g id="Download_x5F_100_x25_"> </g> <g id="Upload"> </g> <g id="Next"> </g> <g id="Last"> </g> <g id="OK"> </g> <g id="Fail"> </g> <g id="Add"> </g> <g id="Spinner_x5F_0_x25_"> </g> <g id="Spinner_x5F_25_x25_"> <g> <path fill="none" stroke="#6d9e37" stroke-width="4" stroke-linejoin="round" stroke-miterlimit="10" d="M50.188,26.812 c12.806,0,23.188,10.381,23.188,23.188"></path> <g> <circle cx="73.375" cy="50" r="1.959"></circle> <circle cx="72.029" cy="57.704" r="1.959"></circle> <circle cx="68.237" cy="64.579" r="1.959"></circle> <circle cx="62.324" cy="69.759" r="1.959"></circle> <circle cx="55.013" cy="72.699" r="1.959"></circle> </g> <g> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.0326 2.3121)" cx="27.045" cy="51.843" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -4.628 2.4912)" cx="28.998" cy="59.416" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.1348 2.8556)" cx="33.325" cy="65.967" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.4877 3.3713)" cx="39.63" cy="70.661" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9968 -0.0794 0.0794 0.9968 -5.6506 3.9762)" cx="47.152" cy="73.012" rx="1.959" ry="1.959"></ellipse> </g> <g> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.713 2.567)" cx="27.71" cy="44.049" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -3.079 2.8158)" cx="30.892" cy="36.872" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.567 3.266)" cx="36.334" cy="31.199" rx="1.959" ry="1.959"></ellipse> <ellipse transform="matrix(0.9962 -0.0867 0.0867 0.9962 -2.2318 3.8617)" cx="43.363" cy="27.636" rx="1.959" ry="1.959"></ellipse> </g> </g> </g> <g id="Spinner_x5F_50_x25_"> </g> <g id="Spinner_x5F_75_x25_"> </g> <g id="Brightest_x5F_25_x25_"> </g> <g id="Brightest_x5F_50_x25_"> </g> <g id="Brightest_x5F_75_x25_"> </g> <g id="Brightest_x5F_100_x25_"> </g> <g id="Reload"> </g> <g id="Forbidden"> </g> <g id="Clock"> </g> <g id="Compass"> </g> <g id="World"> </g> <g id="Speed"> </g> <g id="Microphone"> </g> <g id="Options"> </g> <g id="Chronometer"> </g> <g id="Lock"> </g> <g id="User"> </g> <g id="Position"> </g> <g id="No_x5F_Signal"> </g> <g id="Low_x5F_Signal"> </g> <g id="Mid_x5F_Signal"> </g> <g id="High_x5F_Signal"> </g> <g id="Options_1_"> </g> <g id="Flash"> </g> <g id="No_x5F_Signal_x5F_02"> </g> <g id="Low_x5F_Signal_x5F_02"> </g> <g id="Mid_x5F_Signal_x5F_02"> </g> <g id="High_x5F_Signal_x5F_02"> </g> <g id="Favorite"> </g> <g id="Search"> </g> <g id="Stats_x5F_01"> </g> <g id="Stats_x5F_02"> </g> <g id="Turn_x5F_On_x5F_Off"> </g> <g id="Full_x5F_Height"> </g> <g id="Full_x5F_Width"> </g> <g id="Full_x5F_Screen"> </g> <g id="Compress_x5F_Screen"> </g> <g id="Chat"> </g> <g id="Bluetooth"> </g> <g id="Share_x5F_iOS"> </g> <g id="Share_x5F_Android"> </g> <g id="Love__x2F__Favorite"> </g> <g id="Hamburguer"> </g> <g id="Flying"> </g> <g id="Take_x5F_Off"> </g> <g id="Land"> </g> <g id="City"> </g> <g id="Nature"> </g> <g id="Pointer"> </g> <g id="Prize"> </g> <g id="Extract"> </g> <g id="Play"> </g> <g id="Pause"> </g> <g id="Stop"> </g> <g id="Forward"> </g> <g id="Reverse"> </g> <g id="Next_1_"> </g> <g id="Last_1_"> </g> <g id="Empty_x5F_Basket"> </g> <g id="Add_x5F_Basket"> </g> <g id="Delete_x5F_Basket"> </g> <g id="Error_x5F_Basket"> </g> <g id="OK_x5F_Basket"> </g> </g></svg>
)
}
</>
);
}
if (!isLoggedIn) {
return (
<>
{deviceType === 'desktop' && (
<a href="/login" className="hover:text-[#6d9e37] transition-colors">Login</a>
)}
{deviceType === 'mobile' && (
<a href="/login" className="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="10" r="3"></circle>
<path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path>
</svg>
<span className="text-xs mt-1">Login</span>
</a>
)}
</>
);
}
return (
<>
{deviceType === 'desktop' && (
<a href="/profile" className="hover:text-[#6d9e37] transition-colors">Profile</a>
)}
{deviceType === 'mobile' && (
<a href="/profile" className="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="10" r="3"></circle>
<path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path>
</svg>
<span className="text-xs mt-1">Profile</span>
</a>
)}
</>
);
}

View File

@ -0,0 +1,591 @@
import React, { useState, useEffect } from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../../components/ui/loader";
import { ChevronUp, ChevronDown, Eye, Pencil, Trash2, Download, CircleArrowRight } from "lucide-react";
import { Button } from "../../components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
export default function AllCustomerList() {
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [usersData, setUsersData] = useState([]);
const [dataLoading, setDataLoading] = useState(true);
const [apiError, setApiError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
if (isLoggedIn && sessionData?.user_type === 'admin') {
fetchUsersData();
}
}, [isLoggedIn, sessionData]);
const fetchUsersData = async () => {
setDataLoading(true);
try {
const res = await fetch(`${API_URL}?query=get-all-users`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const data = await res.json();
setUsersData(data.data || []);
} catch (err) {
setApiError(err.message);
} finally {
setDataLoading(false);
}
};
const handleSearch = (e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
};
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
const handleEdit = (user) => {
setCurrentUser(user);
setIsEditModalOpen(true);
};
const handleViewUser = (user) => {
setCurrentUser(user);
setIsViewModalOpen(true)
}
const handleDelete = (user) => {
setCurrentUser(user);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
try {
const res = await fetch(`${API_URL}${currentUser.id}`, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
// Refresh the user list
await fetchUsersData();
setIsDeleteModalOpen(false);
} catch (err) {
setApiError(err.message);
}
};
const saveUserChanges = async (updatedUser) => {
try {
const res = await fetch(`${API_URL}${updatedUser.id}`, {
method: 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedUser)
});
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
// Refresh the user list
await fetchUsersData();
setIsEditModalOpen(false);
} catch (err) {
setApiError(err.message);
}
};
const filteredData = usersData.filter(user => {
const searchLower = searchTerm.toLowerCase();
return (
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.siliconId.toLowerCase().includes(searchLower) ||
user.pbId.toLowerCase().includes(searchLower)
);
});
const sortedData = [...filteredData].sort((a, b) => {
if (sortConfig.key) {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? -1 : 1;
}
if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'asc' ? 1 : -1;
}
}
return 0;
});
// Pagination logic
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
const currentItems = sortedData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const paginate = (pageNumber) => setCurrentPage(pageNumber);
const getPaginationRange = () => {
const totalPageCount = totalPages;
const currentPageNum = currentPage;
const siblingCount = 1;
const DOTS = '...';
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
const totalPageNumbers = siblingCount + 5;
if (totalPageNumbers >= totalPageCount) {
return range(1, totalPageCount);
}
const leftSiblingIndex = Math.max(currentPageNum - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPageNum + siblingCount,
totalPageCount
);
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = totalPageCount;
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [...leftRange, DOTS, totalPageCount];
}
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
return [firstPageIndex, DOTS, ...rightRange];
}
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
}
};
const range = (start, end) => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
const formatDate = (dateString) => {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
};
if (loading || dataLoading) return <Loader />;
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<h1 className="text-2xl font-bold">Customer Management</h1>
<div className="relative w-full md:w-64">
<input
type="text"
placeholder="Search customers..."
className="w-full pl-4 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
value={searchTerm}
onChange={handleSearch}
/>
<svg
className="absolute right-3 top-2.5 h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</div>
</div>
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('siliconId')}
>
<div className="flex items-center">
Silicon ID
{sortConfig.key === 'siliconId' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('name')}
>
<div className="flex items-center">
Name
{sortConfig.key === 'name' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('email')}
>
<div className="flex items-center">
Email
{sortConfig.key === 'email' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('pbId')}
>
<div className="flex items-center">
PB ID
{sortConfig.key === 'pbId' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('type')}
>
<div className="flex items-center">
Type
{sortConfig.key === 'type' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('created_at')}
>
<div className="flex items-center">
Created At
{sortConfig.key === 'created_at' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentItems.length > 0 ? (
currentItems.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{user.siliconId}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{user.pbId}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.type === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-blue-100 text-blue-800' }`}>
{user.type.charAt(0).toUpperCase() + user.type.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(user.created_at)}
</td>
<td className="inline-flex px-6 py-4 space-x-4 whitespace-nowrap text-sm font-medium">
<button onClick={() => window.open(`/manager/view-as?siliconId=${user.siliconId}`)} title={`View as ${user.name}`}>
<CircleArrowRight className="text-[#6d9e37] w-5 h-5 hover:bg-gray-700 rounded-full transition-all transform hover:scale-105 duration-500" />
</button>
<button title="View User" onClick={() => {handleViewUser(user)}} className="text-indigo-600 hover:text-indigo-900">
<Eye className="w-5 h-5" />
</button>
<button title="Edit User" onClick={() => handleEdit(user)} className="text-yellow-600 hover:text-yellow-900">
<Pencil className="w-5 h-5" />
</button>
<button title="Delete User" onClick={() => handleDelete(user)} className="text-red-600 hover:text-red-900">
<Trash2 className="w-5 h-5" />
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
No customers found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{sortedData.length > itemsPerPage && (
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {currentPage} of {totalPages}
</div>
<div className="flex flex-wrap gap-2">
{currentPage > 1 && (
<>
<Button onClick={() => paginate(1)} disabled={currentPage === 1} variant="outline" size="sm">First</Button>
<Button onClick={() => paginate(currentPage - 1)} disabled={currentPage === 1} variant="outline" size="sm">Previous</Button>
</>
)}
{getPaginationRange().map((item, index) => {
if (item === '...') {
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">...</span>
);
}
return (
<Button key={`page-${item}`} onClick={() => paginate(item)} variant={currentPage === item ? "default" : "outline"} size="sm">{item}</Button>
);
})}
{currentPage < totalPages && (
<>
<Button onClick={() => paginate(currentPage + 1)} disabled={currentPage === totalPages} variant="outline" size="sm">Next</Button>
<Button onClick={() => paginate(totalPages)} disabled={currentPage === totalPages} variant="outline" size="sm">Last</Button>
</>
)}
</div>
</div>
)}
{/* Edit Modal */}
{isEditModalOpen && currentUser && (
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
<DialogDescription>
Make changes to the user profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="name" className="text-right">
Name
</label>
<input
id="name"
type="text"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.name || ''}
onChange={(e) => setCurrentUser({...currentUser, name: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="email" className="text-right">
Email
</label>
<input
id="email"
type="email"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.email || ''}
onChange={(e) => setCurrentUser({...currentUser, email: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="type" className="text-right">
Type
</label>
<select
id="type"
className="col-span-3 border rounded-md px-3 py-2"
value={currentUser?.type || 'user'}
onChange={(e) => setCurrentUser({...currentUser, type: e.target.value})}
>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsEditModalOpen(false)}>Cancel</Button>
<Button type="button" onClick={() => {saveUserChanges(currentUser); setIsEditModalOpen(false);}}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* View User Modal */}
{isViewModalOpen && currentUser && (
<Dialog open={isViewModalOpen} onOpenChange={setIsViewModalOpen}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-2xl">User Details</DialogTitle>
<DialogDescription>
Detailed information about {currentUser.name}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Avatar
</div>
<div className="col-span-3">
{currentUser.avatar ? (
<img src={currentUser.avatar} alt={`${currentUser.name}'s avatar`} className="h-16 w-16 rounded-full object-cover" />
) : (
<div className="h-16 w-16 rounded-full bg-gray-200 flex items-center text-center text-sm justify-center text-gray-500">
No Image
</div>
)}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Full Name
</div>
<div className="col-span-3 font-medium">
{currentUser.name}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Email
</div>
<div className="col-span-3">
<a
href={`mailto:${currentUser.email}`}
className="text-blue-600 hover:underline"
>
{currentUser.email}
</a>
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
User Type
</div>
<div className="col-span-3">
{currentUser.type && (
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${
currentUser.type === 'admin'
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
}`}>
{currentUser.type.charAt(0).toUpperCase() + currentUser.type.slice(1)}
</span>
)}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Silicon ID
</div>
<div className="col-span-3 font-mono">
{currentUser.siliconId}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
PB ID
</div>
<div className="col-span-3 font-mono">
{currentUser.pbId}
</div>
</div>
<div className="grid grid-cols-5 items-center gap-4">
<div className="col-span-2 text-right font-medium text-gray-500">
Account Created
</div>
<div className="col-span-3">
{formatDate(currentUser.created_at)}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsViewModalOpen(false)}
className="mt-4"
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && currentUser && (
<Dialog open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Deletion</DialogTitle>
<DialogDescription>
Are you sure you want to delete user {currentUser?.name} ({currentUser?.email})?
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
<Button onClick={() => {confirmDelete();setIsDeleteModalOpen(false);}}>Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
);
}

View File

@ -3,13 +3,14 @@ import { useIsLoggedIn } from '../../lib/isLoggedIn';
import Loader from "../../components/ui/loader";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../../lib/InvoicePDF";
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search, ArrowLeft, FileText } from "lucide-react";
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search, ArrowLeft, FileText, ArrowRight } from "lucide-react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
import * as XLSX from 'xlsx';
export default function AllSellingList() {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [billingData, setBillingData] = useState([]);
const [usersData, setUsersData] = useState([]);
@ -18,17 +19,7 @@ export default function AllSellingList() {
const [selectedItem, setSelectedItem] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [formData, setFormData] = useState({
service: '',
serviceId: 'service-1',
cycle: 'monthly',
amount: '',
user: '',
siliconId: '',
name: '',
status: 'pending',
remarks: ''
});
const [formData, setFormData] = useState({ service: '', serviceId: 'service-1', cycle: 'monthly', amount: '', user: '', siliconId: '', name: '', status: 'pending', remarks: '' });
const [currentStep, setCurrentStep] = useState(1);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [customerSearchTerm, setCustomerSearchTerm] = useState('');
@ -127,11 +118,11 @@ export default function AllSellingList() {
setDialogOpen(true);
};
const handleDeleteItem = async (id) => {
const handleDeleteItem = async (billingId, serviceType) => {
if (!window.confirm('Are you sure you want to delete this billing record?')) return;
try {
const res = await fetch(`${INVOICE_API_URL}?query=delete-billing&id=${id}`, {
const res = await fetch(`${INVOICE_API_URL}?query=delete-billing&billingId=${billingId}&serviceType=${serviceType}`, {
method: 'DELETE',
credentials: 'include',
headers: {
@ -159,9 +150,7 @@ export default function AllSellingList() {
const handleSubmit = async (e) => {
e.preventDefault();
try {
const url = selectedItem
? `${INVOICE_API_URL}?query=update-billing&id=${selectedItem.id}`
: `${INVOICE_API_URL}?query=create-billing`;
const url = selectedItem ? `${INVOICE_API_URL}?query=update-billing&id=${selectedItem.id}` : `${INVOICE_API_URL}?query=create-billing`;
const method = selectedItem ? 'PUT' : 'POST';
@ -218,6 +207,7 @@ export default function AllSellingList() {
'Amount': item.amount,
'cycle': item.cycle,
'Status': item.status,
'Service Status': item.service_status,
'Created At': formatDate(item.created_at),
'Remarks': item.remarks
})));
@ -367,20 +357,29 @@ export default function AllSellingList() {
}).format(parseFloat(amount));
};
if (loading || dataLoading) return <Loader />;
if (error || apiError) return <p>Error: {error?.message || apiError}</p>;
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && dataLoading)) {
return <Loader />;
}
if (error || apiError) return <p>Error: {error?.message || apiError.message}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<section className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Billing Management</h1>
<div className="flex gap-2">
<Button onClick={handleCreateNew} variant="outline">
Create New
</Button>
<div className="relative inline-block">
<Button onClick={() => setIsDropdownOpen(!isDropdownOpen)} variant={isDropdownOpen ? 'default' : 'outline'}>Manage Users</Button>
{isDropdownOpen && (
<div className="absolute mt-1 w-44 bg-white border rounded-md shadow-lg z-10">
<a href="/manager/customer-lists" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Customer</a>
<a href="/manager/selling-list" className="block px-4 py-2 hover:bg-gray-100 rounded-md text-sm text-[#6d9e37] font-medium">Manage Billing</a>
</div>
)}
</div>
<Button onClick={handleCreateNew} variant="outline">Create New</Button>
<Button onClick={exportToExcel} variant="outline" className="flex items-center gap-2">
<FileText className="w-4 h-4" /> Export to Excel
</Button>
@ -532,8 +531,7 @@ export default function AllSellingList() {
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('status')}
>
onClick={() => requestSort('status')}>
<div className="flex items-center">
Status
{sortConfig.key === 'status' && (
@ -545,8 +543,17 @@ export default function AllSellingList() {
</th>
<th
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={() => requestSort('created_at')}
>
onClick={() => requestSort('service_status')}>
<div className="flex items-center">
Ser.. Status
{sortConfig.key === 'service_status' && (
sortConfig.direction === 'asc' ?
<ChevronUp className="ml-1 h-4 w-4" /> :
<ChevronDown className="ml-1 h-4 w-4" />
)}
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" onClick={() => requestSort('created_at')}>
<div className="flex items-center">
Created At
{sortConfig.key === 'created_at' && (
@ -556,35 +563,28 @@ export default function AllSellingList() {
)}
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentItems.length > 0 ? (
currentItems.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{item.billing_id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.service}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.name ?? item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{item.user}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">
{formatCurrency(item.amount)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{item.billing_id}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.service}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.name ?? item.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{item.user}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-medium">{formatCurrency(item.amount)}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusColor(item.status)}`}>
{item.status.charAt(0).toUpperCase() + item.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${item.service_status == 1 ? 'bg-green-100 text-green-800' : item.service_status == 0 ? 'bg-red-100 text-red-800' : ''}`}>
{item.service_status == 1 ? 'Active' : item.service_status == 0 ? 'Deactivate' : ''}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(item.created_at)}
</td>
@ -605,7 +605,7 @@ export default function AllSellingList() {
</button>
<button
title="Delete Items"
onClick={() => handleDeleteItem(item.id)}
onClick={() => handleDeleteItem(item.billing_id, item.service_type)}
className="text-red-600 hover:text-red-900 mr-2"
>
<Trash2 className="w-5 h-5" />
@ -640,59 +640,29 @@ export default function AllSellingList() {
Page {currentPage} of {totalPages}
</div>
<div className="flex flex-wrap gap-2">
<Button
onClick={() => paginate(1)}
disabled={currentPage === 1}
variant="outline"
size="sm"
>
First
</Button>
<Button
onClick={() => paginate(currentPage - 1)}
disabled={currentPage === 1}
variant="outline"
size="sm"
>
Previous
</Button>
{getPaginationRange().map((item, index) => {
if (item === '...') {
{
currentPage > 1 ? (
<>
<Button onClick={() => paginate(1)} disabled={currentPage === 1} variant="outline" size="sm">First</Button>
<Button onClick={() => paginate(currentPage - 1)} disabled={currentPage === 1} variant="outline" size="sm">Previous</Button >
</>
) : ''
}
{
getPaginationRange().map((item, index) => {
if (item === '...') {
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">...</span>
);
}
return (
<span key={`ellipsis-${index}`} className="px-2 py-1">
...
</span>
<Button key={`page-${item}`} onClick={() => paginate(item)} variant={currentPage === item ? "default" : "outline"} size="sm">{item}</Button>
);
}
return (
<Button
key={`page-${item}`}
onClick={() => paginate(item)}
variant={currentPage === item ? "default" : "outline"}
size="sm"
>
{item}
</Button>
);
})}
})
}
<Button
onClick={() => paginate(currentPage + 1)}
disabled={currentPage === totalPages}
variant="outline"
size="sm"
>
Next
</Button>
<Button
onClick={() => paginate(totalPages)}
disabled={currentPage === totalPages}
variant="outline"
size="sm"
>
Last
</Button>
<Button onClick={() => paginate(currentPage + 1)} disabled={currentPage === totalPages} variant="outline" size="sm">Next</Button>
<Button onClick={() => paginate(totalPages)} disabled={currentPage === totalPages} variant="outline" size="sm">Last</Button>
</div>
</div>
)}
@ -727,22 +697,23 @@ export default function AllSellingList() {
/>
</div>
<h3 className="font-semibold mb-4">Select Customer</h3>
<h3 className="font-semibold mb-4 text-neutral-400">Select Customer</h3>
{filteredCustomers.length > 0 ? (
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{filteredCustomers.map(user => (
<div
key={user.id}
onClick={() => handleCustomerSelect(user)}
className="p-3 border rounded-md cursor-pointer hover:bg-gray-50"
className="group p-3 mx-2 border rounded-md cursor-pointer hover:bg-gray-100 hover:border-[#6d9e37] transition-border duration-700"
>
<div className="flex justify-between items-center">
<div>
<p className="font-medium">{user.name}</p>
<p className="text-sm text-gray-600">{user.email}</p>
<p className="font-medium text-neutral-950">{user.name}</p>
<p className="text-sm text-neutral-400">{user.email}</p>
</div>
<div className="text-sm">
<span className="bg-gray-100 px-2 py-1 rounded">ID: {user.siliconId}</span>
<div className="text-sm inline-flex gap-2">
<span className="bg-gray-100 px-2 py-1 rounded text-neutral-950">SiliconId: {user.siliconId}</span>
<ArrowRight className="text-[#6d9e37] opacity-0 group-hover:opacity-100 transition-opacity duration-700" />
</div>
</div>
</div>
@ -758,19 +729,13 @@ export default function AllSellingList() {
<div className="mb-6 p-4 bg-gray-50 rounded-md">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold">Customer Information</h3>
<p className="text-sm"><span className="font-medium">Name:</span> {selectedCustomer.name}</p>
<p className="text-sm"><span className="font-medium">Email:</span> {selectedCustomer.email}</p>
<p className="text-sm"><span className="font-medium">Silicon ID:</span> {selectedCustomer.siliconId}</p>
<h3 className="font-semibold text-neutral-950">Customer Information</h3>
<p className="text-sm text-neutral-400"><span className="font-medium">Name:</span> {selectedCustomer.name}</p>
<p className="text-sm text-neutral-400"><span className="font-medium">Email:</span> {selectedCustomer.email}</p>
<p className="text-sm text-neutral-400"><span className="font-medium">Silicon ID:</span> {selectedCustomer.siliconId}</p>
</div>
{!selectedItem && (
<Button
type="button"
onClick={handleBackToCustomerSelect}
variant="ghost"
size="sm"
className="text-gray-500"
>
<Button type="button" onClick={handleBackToCustomerSelect} variant="ghost" size="sm" className="text-gray-500">
<ArrowLeft className="w-4 h-4 mr-1" /> Change Customer
</Button>
)}
@ -781,23 +746,11 @@ export default function AllSellingList() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Service</label>
<input
type="text"
name="service"
value={formData.service}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
required
/>
<input type="text" name="service" value={formData.service} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" required />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">cycle</label>
<select
name="cycle"
value={formData.cycle}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<select name="cycle" value={formData.cycle} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
<option value="one-time">One-time</option>
@ -805,25 +758,11 @@ export default function AllSellingList() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Amount</label>
<input
type="number"
name="amount"
value={formData.amount}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
step="0.01"
min="0"
required
/>
<input type="number" name="amount" value={formData.amount} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" step="0.01" min="0" required />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
name="status"
value={formData.status}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
>
<select name="status" value={formData.status} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950 bg-white" >
<option value="pending">Pending</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
@ -831,45 +770,17 @@ export default function AllSellingList() {
</div>
<div className="">
<label htmlFor="billing_date" className="block text-sm font-medium text-gray-700 mb-1">Billing Date</label>
<input
type="date"
name="billing_date"
value={formData.billing_date}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
required
/>
<input type="date" name="billing_date" value={formData.billing_date} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
</div>
<div className="">
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
<input
type="text"
name="remarks"
value={formData.remarks}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none"
required
/>
<input type="text" name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
</div>
</div>
<DialogFooter>
<Button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{selectedItem ? 'Update' : 'Create'}
</Button>
<Button
type="button"
onClick={() => {
setDialogOpen(false);
setCurrentStep(1);
}}
variant="outline"
>
Cancel
</Button>
<Button type="submit" className="" >{selectedItem ? 'Update' : 'Create'}</Button>
<Button type="button" onClick={() => { setDialogOpen(false); setCurrentStep(1);}} variant="outline">Cancel</Button>
</DialogFooter>
</form>
) : (

View File

@ -0,0 +1,214 @@
import React, { useState, useEffect } from "react";
import { useToast } from "../ui/toast";
import Loader from "../ui/loader";
import { Button } from "../ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import Table from "../ui/table"; // Import your custom Table component
import { useIsLoggedIn } from '../../lib/isLoggedIn';
export default function HetznerServicesList() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(true);
const [servers, setServers] = useState([]);
const HETZNER_API_KEY = "uMSBR9nxdtbuvazsVM8YMMDd0PvuynpgJbmzFIO47HblMlh7tlHT8wV05sQ28Squ"; // Replace with your actual API key
// Define fetchServers outside useEffect so it can be reused
const fetchServers = async () => {
try {
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
headers: { "Authorization": `Bearer ${HETZNER_API_KEY}` }
});
const data = await response.json();
setServers(data.servers || []);
} catch (error) {
showToast({
title: "Error",
description: "Failed to load servers",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchServers();
}, []);
const handlePowerAction = async (serverId, action) => {
try {
setIsLoading(true);
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}/actions/${action}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
}
});
const data = await response.json();
if (data.action) {
showToast({
title: "Success",
description: `Server ${action} action initiated`,
variant: "success"
});
// Refresh server list after a short delay
setTimeout(() => {
fetchServers();
}, 3000);
}
} catch (error) {
showToast({
title: "Error",
description: `Failed to ${action} server`,
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const handleDeleteServer = async (serverId) => {
try {
setIsLoading(true);
const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${HETZNER_API_KEY}`,
"Content-Type": "application/json"
}
});
if (response.ok) {
showToast({
title: "Success",
description: "Server deleted successfully",
variant: "success"
});
// Remove the server from local state
setServers(prev => prev.filter(server => server.id !== serverId));
}
} catch (error) {
showToast({
title: "Error",
description: "Failed to delete server",
variant: "destructive"
});
} finally {
setIsLoading(false);
}
};
const getStatusBadge = (status) => {
let colorClass = "";
switch (status) {
case "running":
colorClass = "bg-green-100 text-green-800";
break;
case "off":
colorClass = "bg-gray-100 text-gray-800";
break;
case "starting":
case "stopping":
colorClass = "bg-yellow-100 text-yellow-800";
break;
default:
colorClass = "bg-blue-100 text-blue-800";
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
{status}
</span>
);
};
// Prepare data for your Table component
const tableData = servers.map(server => ({
name: server.name,
status: getStatusBadge(server.status),
type: server.server_type.name,
location: server.datacenter.location.city,
ip: server.public_net.ipv4 ? server.public_net.ipv4.ip : "None",
backups: server.backup_window ? "Enabled" : "Disabled",
actions: (
<div className="flex gap-2">
{server.status === "running" ? (
<>
<Button
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "shutdown")}
disabled={isLoading}
>
Stop
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "reboot")}
disabled={isLoading}
>
Reboot
</Button>
</>
) : (
<Button
className=""
variant="outline"
size="sm"
onClick={() => handlePowerAction(server.id, "poweron")}
disabled={isLoading}
>
Start
</Button>
)}
<Button
className="bg-red-500 hover:bg-red-600 transition duration-500"
size="sm"
onClick={() => handleDeleteServer(server.id)}
disabled={isLoading}
>
Delete
</Button>
</div>
)
}));
const tableHeaders = ["Name", "Status", "Type", "Location", "IPv4", "Backups", "Actions"];
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && isLoading && servers.length === 0)) {
return <Loader />;
}
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<Card className="container mx-auto my-4">
<CardHeader>
<CardTitle>Your Hetzner Cloud Servers</CardTitle>
</CardHeader>
<CardContent>
{servers.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">No servers found. Create your first server to get started.</p>
</div>
) : (
<Table
headers={tableHeaders}
data={tableData}
striped
hover
caption="A list of your Hetzner Cloud servers"
className="mt-4"
/>
)}
</CardContent>
</Card>
);
}

View File

View File

@ -0,0 +1,246 @@
import React, {useState, useEffect} from "react";
import { useIsLoggedIn } from '../../lib/isLoggedIn';
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { Button } from "../ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Separator } from "../ui/separator";
import { Eye, Download } from "lucide-react";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../../lib/InvoicePDF";
import Loader from "../ui/loader";
import { localizeTime } from "../../lib/localizeTime";
export default function ViewAsUser() {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [userData, setUserData] = useState(null);
const [billingData, setBillingData] = useState([]);
const [dataLoading, setDataLoading] = useState(false);
const [dataError, setDataError] = useState(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedInvoice, setSelectedInvoice] = useState(null);
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('siliconId');
if (id) {
fetchUserData(id);
}
}, []);
const fetchUserData = async (id) => {
setDataLoading(true);
setDataError(null);
try {
const response = await fetch(
`${USER_API_URL}?query=login-from-admin&siliconId=${id}`,
{
method: 'GET',
credentials: 'include',
headers: { 'Accept': 'application/json' }
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
setUserData(data.user || null);
setBillingData(data.billing || []);
} else {
throw new Error(data.message || 'Failed to fetch data');
}
} catch (err) {
console.error('Error fetching data:', err);
setDataError(err.message);
} finally {
setDataLoading(false);
}
};
const handleViewInvoice = (invoice) => {
setSelectedInvoice(invoice);
setDialogOpen(true);
};
if (loading || (isLoggedIn && sessionData?.user_type === 'admin' && dataLoading)) {
return <Loader />;
}
if (error || dataError) return <p>Error: {error?.message || dataError.message}</p>;
if (!isLoggedIn || sessionData?.user_type !== 'admin') {
return <p className="text-center mt-8">You are not authorized to view this page. <a href="/" className="text-[#6d9e37]">Click Here</a> to go to the homepage.</p>;
}
return (
<div className="space-y-6 container mx-auto p-4">
<h1 className="text-2xl font-bold">View as {userData.name || 'User'}</h1>
<Separator />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<div className="flex-1 lg:max-w-3xl">
<Card>
<CardHeader>
<CardTitle>User Information</CardTitle>
<CardDescription>
Details for user: {userData.siliconid}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-16 w-16">
<AvatarImage src={userData.avatar || ''} />
<AvatarFallback>
{userData.name ? userData.name.charAt(0).toUpperCase() : 'U'}
</AvatarFallback>
</Avatar>
<div>
<h3 className="text-lg font-medium">{userData.name}</h3>
<p className="text-sm text-gray-500">{userData.email}</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<p className="text-sm font-medium">Silicon ID</p>
<p className="text-sm">{userData.siliconId}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">PB ID</p>
<p className="text-sm">{userData.pbId}</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Account Created</p>
<p className="text-sm">
{userData.created_at ? localizeTime(userData.created_at) : 'N/A'}
</p>
</div>
<div className="space-y-2">
<p className="text-sm font-medium">Last Updated</p>
<p className="text-sm">
{userData.updated_at ? localizeTime(userData.updated_at) : 'N/A'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="mt-6">
<CardHeader>
<CardTitle>Billing Information</CardTitle>
<CardDescription>
Billing history for this user
</CardDescription>
</CardHeader>
<CardContent>
{billingData.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr>
<th className="text-left">Invoice ID</th>
<th className="text-center">Date</th>
<th className="text-left">Description</th>
<th className="text-center">Amount</th>
<th className="text-center">Status</th>
<th className="text-center">Actions</th>
</tr>
</thead>
<tbody>
{billingData.map((invoice) => (
<tr key={invoice.id} className="">
<td>{invoice.billing_id}</td>
<td className="text-center">
{invoice?.created_at?.split(' ')[0] || 'N/A'}
</td>
<td className="line-clamp-1">{invoice.service}</td>
<td className="text-center">${invoice.amount}</td>
<td className={`text-center text-sm rounded-full h-fit ${
invoice.status === 'pending' ? 'text-yellow-500' :
invoice.status === 'completed' ? 'text-green-500' :
'text-red-500'
}`}>
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
</td>
<td className="text-center flex justify-center items-center gap-2 p-2">
<button
onClick={() => handleViewInvoice(invoice)}
className="text-gray-600 hover:text-gray-900"
>
<Eye className="w-5 h-5" />
</button>
<PDFDownloadLink
document={<InvoicePDF data={invoice} />}
fileName={`invoice_${invoice.billing_id}.pdf`}
className="text-gray-600 hover:text-gray-900"
>
{({ loading }) => (loading ? '...' : <Download className="w-5 h-5" />)}
</PDFDownloadLink>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-center text-gray-500 py-4">No billing records found</p>
)}
</CardContent>
</Card>
</div>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Invoice Details</DialogTitle>
<DialogDescription>
Details for Invoice ID: <span className="font-bold">{selectedInvoice?.billing_id}</span>
</DialogDescription>
</DialogHeader>
{selectedInvoice && (
<div className="mt-4 space-y-4 text-sm text-gray-700">
<div className="flex justify-between">
<span className="font-bold">Service:</span>
<span>{selectedInvoice.service}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Amount:</span>
<span>${selectedInvoice.amount}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Status:</span>
<span className={`px-2 py-0.5 rounded text-white text-xs ${
selectedInvoice.status === 'pending' ? 'bg-yellow-500' :
selectedInvoice.status === 'completed' ? 'bg-green-500' :
'bg-red-500'
}`}>
{selectedInvoice.status}
</span>
</div>
<Separator className="my-2" />
<div className="flex justify-between">
<span className="font-bold">Created At:</span>
<span>{localizeTime(selectedInvoice.created_at)}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">Updated At:</span>
<span>{selectedInvoice.updated_at ? localizeTime(selectedInvoice.updated_at) : '—'}</span>
</div>
</div>
)}
<DialogFooter className="mt-6">
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -328,7 +328,8 @@ const NewTopic = () => {
</button>
</div>
<div data-color-mode="light">
<MDEditor
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}

View File

@ -0,0 +1,286 @@
import { useState, useEffect } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Label } from "./ui/label";
// import type { RecordModel } from 'pocketbase';
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
interface RecordModel {
id: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
[key: string]: any;
}
interface UserRecord extends RecordModel {
email: string;
verified?: boolean;
emailVisibility?: boolean;
username?: string;
name?: string;
avatar?: string;
provider?: string;
providerData?: {
[key: string]: {
id: string;
name?: string;
email?: string;
avatarUrl?: string;
}
};
lastPasswordReset?: string;
passwordHash?: string;
}
const PasswordUpdateCard = ({ userId, onLogout }: { userId: string, onLogout: () => void }) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [status, setStatus] = useState({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const [isOAuthUser, setIsOAuthUser] = useState(false);
useEffect(() => {
const checkOAuthStatus = async () => {
try {
// Get user with all fields (including internal ones)
const user = await pb.collection('users').getOne(userId, {
fields: '*,provider,verified,passwordHash'
});
// New reliable OAuth detection logic:
const isOAuthUser = (
// Case 1: No password but verified (typical OAuth pattern)
(!user.passwordHash && user.verified) ||
// Case 2: Has provider specified (older OAuth users)
(user.provider && user.provider !== 'email') ||
// Case 3: Check externalId field if exists (some OAuth implementations)
(user.externalId && typeof user.externalId === 'string')
);
console.log('OAuth determination:', {
email: user.email,
hasPassword: !!user.passwordHash,
verified: user.verified,
provider: user.provider,
isOAuthUser
});
setIsOAuthUser(isOAuthUser);
} catch (error) {
console.error("Failed to fetch user data:", error);
// Fallback to checking verification status
setIsOAuthUser(pb.authStore.model?.verified === true);
}
};
checkOAuthStatus();
}, [userId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
try {
// Validate inputs
if (newPassword !== confirmPassword) {
throw new Error("New passwords don't match");
}
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters");
}
// SPECIAL HANDLING FOR OAUTH USERS
if (isOAuthUser) {
// For OAuth users, we need to use a different endpoint or method
// since the regular update requires oldPassword
const authData = await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword
}, {
// This header might be needed depending on your PocketBase version
headers: { 'X-Require-Password-Confirm': 'false' }
});
} else {
// Regular password update flow
await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword,
oldPassword: currentPassword
});
}
// Clear form
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setStatus({
message: isOAuthUser
? 'Password successfully set! You can now login with email'
: 'Password updated successfully!',
isError: false
});
// Logout user after password change
onLogout();
} catch (error: any) {
console.error("Password update failed:", error);
// Special handling for OAuth users if the first method fails
if (isOAuthUser && error.data?.oldPassword?.message) {
try {
// Alternative method for OAuth users - create a new auth record
await pb.collection('users').authWithPassword(
pb.authStore.model?.email,
newPassword
);
setStatus({
message: 'Password successfully set!',
isError: false
});
onLogout();
return;
} catch (secondError) {
error = secondError;
}
}
// Handle specific PocketBase validation errors
let errorMessage = error.message;
if (error.data?.oldPassword?.message) {
errorMessage = "Please provide your current password";
} else if (error.data?.password?.message) {
errorMessage = error.data.password.message;
}
setStatus({
message: errorMessage || "Failed to update password. Please try again.",
isError: true
});
} finally {
setIsLoading(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
<CardDescription>
{isOAuthUser
? "Set a password to enable email login"
: "Update your password"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleSubmit}>
{status.message && (
<div className={`p-3 rounded-md text-sm mb-4 ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
{!isOAuthUser && (
<div className="space-y-2">
<Label htmlFor="currentPassword">Current password</Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrentPassword ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required={!isOAuthUser}
/>
<button
type="button"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showCurrentPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="newPassword">
{isOAuthUser ? "Set new password" : "New password"}
</Label>
<div className="relative">
<Input
id="newPassword"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showNewPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Password must be at least 8 characters
</p>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">
{isOAuthUser ? "Confirm new password" : "Confirm password"}
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
className="mt-4 w-full"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{isOAuthUser ? "Setting password..." : "Updating..."}
</>
) : (
isOAuthUser ? "Set Password" : "Update Password"
)}
</Button>
</form>
</CardContent>
</Card>
);
};
export default PasswordUpdateCard;

View File

@ -1,54 +1,192 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { FileDown } from "lucide-react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from './ui/loader';
export default function HestiaCredentialsFetcher() {
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const [creds, setCreds] = useState(null);
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(false);
const [error, setError] = useState('');
const [serviceName, setServiceName] = useState(null);
const [serviceOrderId, setServiceOrderId] = useState(null);
const [userSiliconId, setUserSiliconId] = useState();
const fetchCredentials = async () => {
setLoading(true);
setError('');
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
useEffect(() => {
const fetchData = async () => {
const urlParams = new URLSearchParams(window.location.search);
const service = urlParams.get('service');
const orderId = urlParams.get('orderId');
setUserSiliconId(sessionData.siliconId);
// console.log(sessionData.siliconId)
setServiceName(service);
setServiceOrderId(orderId);
if (service && orderId ) {
await fetchCredentials(service, orderId); // Pass orderId directly
}
};
fetchData();
}, [sessionData]);
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? hostings : '';
try {
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/services/index.php?query=get-hestia-cred', {
method: 'GET', // Changed to GET to match backend
credentials: 'include'
const serviceDataPayload = {
success: servicesData.success,
service: servicesData.service,
password: servicesData.password,
output: servicesData.output,
publicKey: servicesData.publicKey,
endpoint: servicesData.endpoint,
qr_code: servicesData.qr_code,
continent: servicesData.continent,
billingId: currentBillingId,
status: "active"
}
console.log('serviceDataPayload', serviceDataPayload)
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(serviceDataPayload)
});
const data = await response.json();
console.log('Node Backend API', data);
return data; // Return the data for further processing if needed
} catch (error) {
console.error('An error occurred while saving to MongoDB', error);
throw error; // Re-throw to handle in the calling function
}
};
// Update fetchCredentials to accept orderId as parameter
const fetchCredentials = async (serviceToCall, orderIdParam = null) => {
setDataLoading(true);
setError('');
setCreds(null);
try {
// Use the passed orderIdParam or fall back to state
const effectiveOrderId = orderIdParam || serviceOrderId;
const query = serviceToCall === 'vpn' ? 'get-vpn-cred' : 'get-hestia-cred';
const response = await fetch(`${SERVICES_API_URL}?query=${query}&orderId=${effectiveOrderId}`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.error || `HTTP error: ${response.status}`);
}
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || data.details || 'Failed to fetch credentials');
if (sessionData?.siliconId) {
const mongoResponse = await handleSaveDataInMongoDB(data, effectiveOrderId, serviceToCall, sessionData.siliconId);
console.log('mongoResponse', mongoResponse)
}
setCreds(data);
} catch (err) {
console.error(err);
console.error("FETCH ERROR:", err);
setError(err.message);
} finally {
setLoading(false);
setDataLoading(false);
}
};
const downloadConfig = () => {
if (!creds?.output) return;
const element = document.createElement('a');
const file = new Blob([creds.output], { type: 'application/octet-stream' });
element.href = URL.createObjectURL(file);
element.download = `${creds.username || 'vpn-config'}.conf`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
if (loading) {
return <Loader />;
}
// Then handle not logged in state
if (!isLoggedIn) {
window.location.href = '/';
return;
}
return (
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
<h2 className="text-xl font-bold mb-4">Get Hestia Credentials</h2>
<button
onClick={fetchCredentials}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
disabled={loading}
>
{loading ? 'Fetching...' : 'Get Credentials'}
</button>
<div className="space-y-4">
{serviceName === 'vpn' ? (
<div className="p-4 bg-gray-100 rounded shadow max-w-lg mx-auto mt-10">
{
!creds ? (
<>
<h2 className="text-xl font-bold mb-4">Get VPN Credentials</h2>
{!dataLoading && (
<Button onClick={() => fetchCredentials('vpn')} disabled={dataLoading} className="w-full">
{dataLoading ? 'Fetching...' : 'Get Credentials'}
</Button>
)}
</>
) : (
<>
<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>
</>
)
}
{creds && (
<div className="mt-4 bg-white p-4 rounded border">
<p><strong>Username:</strong> {creds.username}</p>
<p><strong>Password:</strong> {creds.password}</p>
<p><strong>Output:</strong> <pre className="whitespace-pre-wrap">{creds.output}</pre></p>
{dataLoading && <p>Loading credentials...</p>}
{creds && creds.service === 'vpn' && (
<div className="mt-4 bg-white p-4 rounded border space-y-4">
{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>
)}
{creds.output && (
<div>
<div className="flex justify-between items-center mb-2">
<h3 className="font-semibold text-neutral-950">Configuration File</h3>
<Button onClick={downloadConfig} size="sm" variant='outline'>
Download .conf &nbsp; <FileDown size={18} />
</Button>
</div>
<pre className="whitespace-pre-wrap bg-gray-50 p-3 rounded text-sm overflow-x-auto text-neutral-950">{creds.output}</pre>
</div>
)}
</div>
)}
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
) : creds && creds.service === 'hestia' && (
<div className="p-4 bg-gray-100 rounded shadow max-w-md mx-auto mt-10">
<h2 className="text-xl font-bold mb-4">Hestia Credentials</h2>
{dataLoading && <p>Loading credentials...</p>}
{creds && creds.service === 'hestia' && (
<div className="mt-4 bg-white p-4 rounded border">
<p><strong>Username:</strong> {creds.username}</p>
<p><strong>Password:</strong> {creds.password}</p>
{creds.output && (
<p><strong>Output:</strong> <pre className="whitespace-pre-wrap">{creds.output}</pre></p>
)}
</div>
)}
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
)}
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
);
}

View File

@ -1,5 +1,9 @@
import React from 'react';
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
export interface ServiceCardProps {
title: string;
@ -7,48 +11,82 @@ export interface ServiceCardProps {
imageUrl: string;
features: string[];
learnMoreUrl: string;
buyButtonText: string;
buyButtonUrl: string;
}
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl }: ServiceCardProps) {
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl, buyButtonText, buyButtonUrl }: ServiceCardProps) {
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const [showLoginModal, setShowLoginModal] = useState(false);
const handleBuyClick = () => {
if (!isLoggedIn) {
setShowLoginModal(true);
return;
}
window.location.href = buyButtonUrl;
};
return (
<Card className="overflow-hidden transition-all hover:shadow-lg dark:border-neutral-700 h-full flex flex-col">
<div className="relative h-48 w-full overflow-hidden bg-neutral-100 dark:bg-neutral-800">
<img
src={imageUrl}
alt={`${title} illustration`}
className="object-cover w-full h-full transition-transform duration-300 hover:scale-105"
/>
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400">{description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2 text-sm">
{features.map((feature, index) => (
<li key={index} className="flex items-start">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className='text-zinc-600'>{feature}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<a
href={learnMoreUrl}
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
>
Learn More
</a>
</CardFooter>
</Card>
<>
<Card className="overflow-hidden transition-all hover:shadow-lg dark:border-neutral-700 h-full flex flex-col">
<div className="relative h-48 w-full overflow-hidden bg-neutral-100 dark:bg-neutral-800">
<img
src={imageUrl}
alt={`${title}`}
className="object-cover w-full h-full transition-transform duration-300 hover:scale-105"
/>
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400">{description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2 text-sm">
{features.map((feature, index) => (
<li key={index} className="flex items-start">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[#6d9e37] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /></svg>
<span className='text-zinc-600'>{feature}</span>
</li>
))}
</ul>
</CardContent>
<CardFooter>
<div className='flex gap-4 w-full'>
{buyButtonText && buyButtonUrl && (
<Button className='w-full' variant='outline' onClick={handleBuyClick}>{buyButtonText}</Button>
)}
<a
href={learnMoreUrl}
className="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors w-full"
>
Learn More
</a>
</div>
</CardFooter>
</Card>
{/* Login Required Modal */}
<Dialog open={showLoginModal} onOpenChange={setShowLoginModal}>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-xl mb-2">Login Required</DialogTitle>
<DialogDescription>
You need to be logged in to purchase this service.
</DialogDescription>
</DialogHeader>
<div className="mt-4 flex flex-col space-y-2">
<Button onClick={() => window.location.href = '/login'} className="w-full bg-[#6d9e37] hover:bg-[#598035]">Login Now</Button>
<Button
variant="outline"
className="w-full"
onClick={() => setShowLoginModal(false)}
>
Continue Browsing
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}
}

View File

@ -0,0 +1,335 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Separator } from "./ui/separator";
import { Label } from "./ui/label";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Loader from "./ui/loader";
interface AuthStatus {
message: string;
isError: boolean;
}
interface UserRecord {
id: string;
email: string;
name?: string;
type?: string;
avatar?: string;
[key: string]: any;
}
interface AuthResponse {
token: string;
record: UserRecord;
}
const SignupPage = () => {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordVisible, setPasswordVisible] = useState(false);
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
const [status, setStatus] = useState<AuthStatus>({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const pb = new PocketBase("https://tst-pb.s38.siliconpin.com");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
if (password !== confirmPassword) {
setStatus({ message: 'Passwords do not match', isError: true });
setIsLoading(false);
return;
}
try {
// Create the user
const userData = {
email,
name,
password,
passwordConfirm: confirmPassword,
emailVisibility: true,
};
const record = await pb.collection('users').create(userData);
// Automatically login after signup
const authData = await pb.collection('users').authWithPassword(email, password);
if (!authData?.token || !authData?.record) {
throw new Error("Authentication failed after signup");
}
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = {
token: authData.token,
record: {
id: authData.record.id,
email: authData.record.email,
name: authData.record.name || authData.record.email.split('@')[0],
type: authData.record.type || 'user',
avatar: authData.record.avatar || '',
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error: any) {
console.error("Signup failed:", error);
setStatus({
message: error.message || "Signup failed. Please try again.",
isError: true
});
} finally {
setIsLoading(false);
}
};
const signupWithOAuth2 = async (provider: 'google' | 'facebook' | 'github') => {
try {
setIsLoading(true);
setStatus({ message: '', isError: false });
const authData = await pb.collection('users').authWithOAuth2({ provider });
if (!authData?.record) {
throw new Error("No user record found");
}
const avatarUrl = authData.record.avatar ? pb.files.getUrl(authData.record, authData.record.avatar) : '';
const authResponse: AuthResponse = {
token: authData.token,
record: {
id: authData.record.id,
email: authData.record.email || '',
name: authData.record.name || '',
type: authData.record.type || '',
avatar: authData.record.avatar || ''
}
};
await syncSessionWithBackend(authResponse, avatarUrl);
window.location.href = '/profile';
} catch (error) {
console.error(`${provider} Signup failed:`, error);
setStatus({
message: `${provider} signup failed. Please try again.`,
isError: true
});
} finally {
setIsLoading(false);
}
};
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
try {
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'new',
accessToken: authData.token,
email: authData.record.email,
name: authData.record.name,
type: authData.record.type,
avatar: avatarUrl,
isAuthenticated: true,
id: authData.record.id
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Session synced with backend:', data);
return data;
} catch (error: any) {
console.error('Error syncing session:', error);
throw new Error(`Session sync failed: ${error.message}`);
}
};
const { isLoggedIn, loading, error } = useIsLoggedIn();
if (loading) {
return <Loader />;
}
if (isLoggedIn) {
window.location.href = '/';
return null;
}
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">
<CardHeader className="text-center space-y-1">
<CardTitle className="text-2xl font-bold">Create an Account</CardTitle>
<CardDescription className="">
Join us to get started
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{status.message && (
<div className={`p-3 rounded-md text-sm ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your Name"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
id="password"
type={passwordVisible ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setPasswordVisible(!passwordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{passwordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<div className="relative">
<Input
id="confirmPassword"
type={confirmPasswordVisible ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
required
minLength={8}
className="focus:ring-2 focus:ring-blue-500 pr-10"
/>
<button
type="button"
onClick={() => setConfirmPasswordVisible(!confirmPasswordVisible)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{confirmPasswordVisible ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating account...
</>
) : (
'Sign Up'
)}
</Button>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or sign up with
</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
<Button
variant="outline"
onClick={() => signupWithOAuth2('google')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/google.svg" alt="Google" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => signupWithOAuth2('facebook')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/facebook.svg" alt="Facebook" className="h-6 w-6" />
</Button>
<Button
variant="outline"
onClick={() => signupWithOAuth2('github')}
disabled={isLoading}
className="flex items-center justify-center gap-2"
>
<img src="/assets/github.svg" alt="GitHub" className="h-6 w-6" />
</Button>
</div>
</CardContent>
<div className="px-6 pb-6 text-center text-sm text-gray-500">
Already have an account?{' '}
<a href="/login" className="font-medium text-[#6d9e37] hover:underline">
Sign in
</a>
</div>
</Card>
</div>
);
};
export default SignupPage;

View File

@ -1,24 +1,221 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { marked } from 'marked';
export default function TopicDetail(props) {
// console.log('All topic Form allTopic', props.allTopic);
const [showCopied, setShowCopied] = useState(false);
if (!props.topic) {
return <div>Topic not found</div>;
}
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
const title = props.topic.title;
const text = `Check out this article: ${title}`;
// const shareOnFacebook = () => {
// window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
// };
// const shareOnTwitter = () => {
// window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank');
// };
// const shareOnLinkedIn = () => {
// window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
// };
// const shareOnReddit = () => {
// window.open(`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
// };
// const shareOnWhatsApp = () => {
// window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${shareUrl}`)}`, '_blank');
// };
// const shareViaEmail = () => {
// window.open(`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${shareUrl}`)}`);
// };
const shareOnSocialMedia = (platform) => {
switch (platform){
case 'facebook':
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
break;
case 'twitter':
window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank');
break;
case 'linkedin':
window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
break;
case 'reddit':
window.open(`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
break;
case 'whatsapp':
window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${shareUrl}`)}`, '_blank');
break;
case 'email':
window.open(`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${shareUrl}`)}`);
break;
}
}
const copyToClipboard = () => {
navigator.clipboard.writeText(shareUrl);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 2000);
};
// SVG Icons with improved styling
const SocialIcon = ({ children, className = "" }) => (
<span className={`w-5 h-5 flex items-center justify-center ${className}`}>
{children}
</span>
);
const FacebookIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"/>
</svg>
);
const TwitterIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 30 30" fill="currentColor">
<path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z"></path>
</svg>
);
const LinkedInIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
</svg>
);
const RedditIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.5.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.12-.07 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.963-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>
);
const WhatsAppIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
);
const MailIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z"/>
</svg>
);
const LinkIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M6.188 8.719c.439-.439.926-.801 1.444-1.087 2.887-1.591 6.589-.745 8.445 2.069l-2.246 2.245c-.644-1.469-2.243-2.305-3.834-1.949-.599.134-1.168.433-1.633.898l-4.304 4.306c-1.307 1.307-1.307 3.433 0 4.74 1.307 1.307 3.433 1.307 4.74 0l1.327-1.327c1.207.479 2.501.67 3.779.575l-2.929 2.929c-2.511 2.511-6.582 2.511-9.093 0s-2.511-6.582 0-9.093l4.304-4.306zm6.836-6.836l-2.929 2.929c1.277-.096 2.572.096 3.779.574l1.326-1.326c1.307-1.307 3.433-1.307 4.74 0 1.307 1.307 1.307 3.433 0 4.74l-4.305 4.305c-1.311 1.311-3.44 1.3-4.74 0-.303-.303-.564-.68-.727-1.051l-2.246 2.245c.236.358.481.689.736 1.011.748.98 1.804 1.644 2.993 1.907 1.535.368 3.159.166 4.613-.617.518-.286 1.005-.648 1.444-1.087l4.304-4.305c2.512-2.511 2.512-6.582.001-9.093-2.511-2.51-6.581-2.51-9.092 0z"/>
</svg>
);
return (
<div className="container mx-auto px-4 py-12">
<article className="max-w-4xl mx-auto">
{
props.topic.img && (
<img src={props.topic.img} alt={props.topic.title} className="w-full h-96 object-cover rounded-lg mb-8" />
)
}
<h1 className="text-4xl font-bold text-[#6d9e37] mb-4">{props.topic.title}</h1>
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}></div>
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video 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 article:</p>
<div className="flex flex-wrap gap-2">
<button
onClick={() => shareOnSocialMedia('facebook')}
className="flex items-center gap-1 px-2 py-2 bg-[#3b5998] hover:bg-[#2d4373] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Facebook"
title="Share on Facebook"
>
<SocialIcon className="text-white"><FacebookIcon /></SocialIcon>
<span className="text-sm font-medium">Facebook</span>
</button>
<button
onClick={() => shareOnSocialMedia('whatsapp')}
className="flex items-center gap-1 px-2 py-2 bg-[#25D366] hover:bg-[#1da851] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on WhatsApp"
title="Share on WhatsApp"
>
<SocialIcon className="text-white"><WhatsAppIcon /></SocialIcon>
<span className="text-sm font-medium">WhatsApp</span>
</button>
<button
onClick={() => shareOnSocialMedia('twitter')}
className="flex items-center gap-1 px-2 py-2 bg-[#000000] hover:bg-[#000000] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Twitter"
title="Share on Twitter"
>
<SocialIcon className="text-white"><TwitterIcon /></SocialIcon>
<span className="text-sm font-medium">Twitter</span>
</button>
<button
onClick={() => shareOnSocialMedia('linkedin')}
className="flex items-center gap-1 px-2 py-2 bg-[#0077b5] hover:bg-[#005582] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on LinkedIn"
title="Share on LinkedIn"
>
<SocialIcon className="text-white"><LinkedInIcon /></SocialIcon>
<span className="text-sm font-medium">LinkedIn</span>
</button>
<button
onClick={() => shareOnSocialMedia('reddit')}
className="flex items-center gap-1 px-2 py-2 bg-[#FF5700] hover:bg-[#e04e00] text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share on Reddit"
title="Share on Reddit"
>
<SocialIcon className="text-white"><RedditIcon /></SocialIcon>
<span className="text-sm font-medium">Reddit</span>
</button>
<button
onClick={() => shareOnSocialMedia('reddit')}
className="flex items-center gap-1 px-2 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Share via Email"
title="Share via Email"
>
<SocialIcon className="text-white"><MailIcon /></SocialIcon>
<span className="text-sm font-medium">Email</span>
</button>
<div className="relative">
<button
onClick={copyToClipboard}
className="flex items-center gap-1 px-2 py-2 bg-gray-800 hover:bg-gray-900 text-white rounded-lg transition-all shadow-sm hover:shadow-md active:scale-95"
aria-label="Copy link"
title="Copy link to share"
>
<SocialIcon className="text-white"><LinkIcon /></SocialIcon>
<span className="text-sm font-medium">Copy Link</span>
</button>
{showCopied && (
<div className="absolute -bottom-8 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs px-2 py-1 rounded whitespace-nowrap shadow-md">
Link copied!
</div>
)}
</div>
</div>
</div>
<div
className="font-light mb-8 text-justify prose max-w-none"
dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}
></div>
</article>
</div>
);
}
}

View File

@ -46,7 +46,7 @@ export default function EditTopic (){
const data = await response.json();
const topic = data.data[0]
console.log('Single topic data', data)
// console.log('Single topic data', data)
setFormData({
status: topic.status || 'draft',
category: topic.category || '',

View File

@ -6,6 +6,7 @@ import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Pencil, Trash2, Search } from "lucide-react";
import { marked } from 'marked';
export default function TopicItems(props) {
console.table(props.topics)
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');
@ -32,13 +33,7 @@ export default function TopicItems(props) {
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">{props.title}</h2>
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">{props.description}</p>
<form onSubmit={handleSubmit} className="flex gap-2 max-w-xl mx-auto mt-4">
<Input
type="text"
name="search"
placeholder="Search Topic..."
value={localSearchTerm}
onChange={handleSearchChange}
/>
<Input type="text" name="search" placeholder="Search Topic..." value={localSearchTerm} onChange={handleSearchChange}/>
<Button type="submit">
<Search className="h-4 w-4 mr-2" />
Search
@ -57,8 +52,18 @@ export default function TopicItems(props) {
</div>
<CardContent className="flex-1 p-6">
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
<CardDescription className="line-clamp-4 mb-4" dangerouslySetInnerHTML={{ __html: marked.parse(topic.content || '') }}>
</CardDescription>
<CardDescription className="line-clamp-4 mb-4 text-justify" dangerouslySetInnerHTML={{ __html: marked.parse(topic.content || '') }}></CardDescription>
<div className="flex justify-between items-center">
<p className="text-xs text-gray-500"><strong>Author: </strong>{topic.user.split('@')[0]}</p>
<p className="text-xs text-gray-500">
<strong>Date: </strong>
{new Date(topic.timestamp).toLocaleDateString('en-GB', {
day: '2-digit',
year: '2-digit',
month: 'short',
}).replace(',', '')}
</p>
</div>
{isLoggedIn && sessionData.user_email === topic.user && props.mytopic === true && (
<div className="flex justify-end gap-2 mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<Button variant="outline" size="sm" className="gap-1"

View File

@ -112,9 +112,9 @@ export default function TopicCreation() {
return (
<>
{isLoggedIn && (
<div className="container mx-auto flex justify-end gap-x-4 mb-4">
<a href="/topic/new" className="create-new-link">Create New</a>
<a href="/topic/my-topic">My Topics</a>
<div className="container mx-auto flex justify-end gap-x-4 my-4">
<Button onClick={() => window.location.href = '/topic/new'} variant="outline">Create New</Button>
<Button onClick={() => window.location.href = '/topic/my-topic'} variant="outline">My Creation</Button>
</div>
)}
@ -124,13 +124,7 @@ export default function TopicCreation() {
<div className="error-message p-4 bg-red-100 text-red-700 rounded">Error loading topics: {error}</div>
) : (
<>
<TopicItems
topics={topics}
title="SoliconPin Topics"
description={topicPageDesc}
onSearch={handleSearch}
searchTerm={searchTerm}
/>
<TopicItems topics={topics} title="SoliconPin Topics" description={topicPageDesc} onSearch={handleSearch} searchTerm={searchTerm}/>
{pagination.last_page > 1 && (
<div className="flex flex-col justify-between items-center mt-8 gap-4">

View File

@ -14,8 +14,8 @@ import Loader from "./ui/loader";
import { Eye, Pencil, Trash2, Download, ChevronUp, ChevronDown, Search } from "lucide-react";
import { PDFDownloadLink } from '@react-pdf/renderer';
import InvoicePDF from "../lib/InvoicePDF";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { useIsLoggedIn } from '../lib/isLoggedIn';
import PasswordUpdateCard from './PasswordUpdateCard';
interface SessionData {
[key: string]: any;
}
@ -52,10 +52,10 @@ export default function ProfilePage() {
const [selectedData, setSelectedData] = useState<BillingItems | null>(null);
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
const [txnId, setTxnId] = useState<string>('');
const [userEmail, setUserEmail] = useState<string>('');
// console.log('isLoggedIn', sessionData.id)
const [userEmail, setUserEmail] = useState<string>('');
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
useEffect(() => {
const fetchSessionData = async () => {
try {
@ -140,21 +140,35 @@ export default function ProfilePage() {
console.log('Logout Console', data.success);
})
}
if (loading) {
return <Loader />;
}
// Then handle not logged in state
if (!isLoggedIn) {
return (
<p className="text-center my-8">
You are not logged in! Please login first to access this page.{" "}
<a className="text-[#6d9e37]" href="/login">Click Here</a> to login
</p>
);
}
// Then handle error state
if (error) {
return <div>Error: {error}</div>;
}
// Then handle case where user data hasn't loaded yet
if (!userData) {
return (<Loader />);
}
{
// console.log('selectedData', selectedData)
return <Loader />;
}
return (
<div className="space-y-6 container mx-auto">
<Separator />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<div className="flex-1 lg:max-w-2xl">
<div className="flex-1 lg:max-w-3xl">
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
@ -212,11 +226,11 @@ export default function ProfilePage() {
</thead>
<tbody>
{
invoiceList.slice(-5).map((invoice, index) => (
invoiceList.slice(0, 5).map((invoice, index) => (
<tr key={index} className="">
<td>{invoice.billing_id}</td>
<td className="text-center">{invoice?.created_at.split(' ')[0]}</td>
<td className="line-clamp-1">{invoice.service}</td>
<td className=""><p className="line-clamp-1">{invoice.service}</p></td>
<td className="text-center">{invoice.amount}</td>
<td className={`text-center text-sm rounded-full h-fit ${invoice.status === 'pending' ? 'text-yellow-500' : invoice.status === 'completed' ? 'text-green-500' : 'text-red-500'}`}>{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}</td>
<td className="text-center flex justify-center items-center gap-2 p-2">
@ -251,30 +265,8 @@ export default function ProfilePage() {
</Card>
</div>
<div className="flex-1 space-y-6 lg:max-w-md">
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
<CardDescription>
Update your password and security settings.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="currentPassword">Current password</Label>
<Input id="currentPassword" type="password" />
</div>
<div className="space-y-2">
<Label htmlFor="newPassword">New password</Label>
<Input id="newPassword" type="password" />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm password</Label>
<Input id="confirmPassword" type="password" />
</div>
<Button className="mt-4">Update password</Button>
</CardContent>
</Card>
<div className="flex-1 space-y-6 lg:max-w-xl">
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut}/>
<Card className="mt-6">
<CardHeader>
<CardTitle>Danger Zone</CardTitle>
@ -307,7 +299,7 @@ export default function ProfilePage() {
</div>
<div className="flex justify-between">
<span className="font-bold">Amount:</span>
<span>${selectedData?.amount}</span>
<span>&#8377;{selectedData?.amount}</span>
</div>
<div className="flex justify-between">
<span className="font-bold">User:</span>
@ -343,3 +335,4 @@ export default function ProfilePage() {
</div>
);
}

View File

@ -5,12 +5,14 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline";
size?: "default" | "sm" | "lg";
type?: "submit" | "button";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", type = '', size = "default", ...props }, ref) => {
({ className, variant = "default", type, size = "default", ...props }, ref) => {
return (
<button
type={type}
className={cn(
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-neutral-900",
{

View File

@ -0,0 +1,29 @@
import React from "react";
export const Switch = ({
checked,
onCheckedChange,
id,
className = "",
...props
}) => {
return (
<button
type="button"
role="switch"
aria-checked={checked}
id={id}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#6d9e37] ${
checked ? "bg-[#6d9e37]" : "bg-gray-200"
} ${className}`}
onClick={() => onCheckedChange(!checked)}
{...props}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
};

View File

@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
export const Toast = ({ message, type, onDismiss, duration = 3000 }) => {
useEffect(() => {
const timer = setTimeout(() => {
onDismiss();
}, duration);
return () => clearTimeout(timer);
}, [duration, onDismiss]);
const bgColor = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500',
}[type] || 'bg-gray-500';
return (
<div className={`fixed bottom-4 right-4 ${bgColor} text-white px-4 py-2 rounded-md shadow-lg flex items-center`}>
<span>{message}</span>
<button
onClick={onDismiss}
className="ml-2 text-white hover:text-gray-200 font-bold"
>
×
</button>
</div>
);
};
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = React.useState([]);
const showToast = (message, options = {}) => {
const id = Date.now();
setToasts((prev) => [...prev, { id, message, ...options }]);
};
const dismissToast = (id) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
};
return (
<>
{children}
{toasts.map((toast) => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
duration={toast.duration}
onDismiss={() => dismissToast(toast.id)}
/>
))}
</>
);
};
export const useToast = () => {
const [toasts, setToasts] = React.useState([]);
const showToast = (message, options = {}) => {
const id = Date.now();
setToasts((prev) => [...prev, { id, message, ...options }]);
setTimeout(() => {
dismissToast(id);
}, options.duration || 3000);
};
const dismissToast = (id) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
};
return { showToast, dismissToast };
};

View File

@ -1,10 +1,11 @@
---
import '../styles/global.css';
import LoginProfile from '../components/LoginOrProfile';
interface Props {
title: string;
description?: string;
image?: string;
ogImage?: string;
canonicalURL?: string;
type?: 'website' | 'article';
}
@ -12,7 +13,7 @@ interface Props {
const {
title,
description = "SiliconPin offers high-performance hosting solutions for PHP, Node.js, Python, K8s, and K3s applications with 24/7 support.",
image = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop",
ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
type = "website",
} = Astro.props;
@ -28,7 +29,7 @@ const organizationSchema = {
"@type": "ContactPoint",
"telephone": "+91-700-160-1485",
"contactType": "customer service",
"availableLanguage": ["English"]
"availableLanguage": ["English", "Hindi", "Bengali"]
},
"address": {
"@type": "PostalAddress",
@ -67,7 +68,7 @@ const organizationSchema = {
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content="SiliconPin" />
<!-- Twitter -->
@ -75,7 +76,7 @@ const organizationSchema = {
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
<meta property="twitter:image" content={ogImage} />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="https://siliconpin.com/assets/logo.svg" />
@ -98,7 +99,8 @@ const organizationSchema = {
<a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a>
<a href="/topic" class="hover:text-[#6d9e37] transition-colors">Topic</a>
<a href="/contact" class="hover:text-[#6d9e37] transition-colors">Contact</a>
<a href="/profile" class="hover:text-[#6d9e37] transition-colors">Profile</a>
<LoginProfile deviceType="desktop" client:load />
<!-- <a href="/profile" class="hover:text-[#6d9e37] transition-colors"></a> -->
<a href="/get-started" class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors">Get Started</a>
</nav>
<!-- Mobile menu button -->
@ -234,15 +236,15 @@ const organizationSchema = {
<svg width="20px" height="20px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>about</title> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="about-white" fill="#6d9e37" transform="translate(42.666667, 42.666667)"> <path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.44,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.44,384 213.333333,384 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z" id="Shape"> </path> </g> </g> </g></svg>
<span class="text-xs mt-1">About</span>
</a>
<a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<LoginProfile deviceType="mobile" client:load />
<!-- <a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
<span class="text-xs mt-1">Profile</span>
</a>
</a> -->
</div>
</nav>
</body>
</html>
<script is:inline>
// Mobile menu toggle functionality
document.addEventListener('DOMContentLoaded', function() {

View File

@ -10,7 +10,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">

View File

@ -48,7 +48,7 @@ const contactSchema = {
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<script type="application/ld+json" set:html={JSON.stringify(contactSchema)}></script>

View File

@ -16,7 +16,7 @@ const defaultSubdomain = randomString();
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
@ -249,7 +249,7 @@ const defaultSubdomain = randomString();
</div>
</div>
<script>
<script is:inline>
// Get DOM elements
const deploymentType = document.getElementById('deployment-type');
const appOptions = document.getElementById('app-options');

View File

@ -17,7 +17,7 @@ const defaultSubdomain = randomString();
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">

View File

@ -4,13 +4,13 @@ import Layout from '../layouts/Layout.astro';
// Page-specific SEO metadata
const pageTitle = "SiliconPin - Lets create some digital freedom";
const pageDescription = "SiliconPin - easy to deploy apps and tools, freedom oriented apps and tools, high-performance, hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s, and technical support.";
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
const pageImage = "/assets/logo.svg";
---
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
>
<!-- Canvas background animation -->
<div class="fixed inset-0 z-0 opacity-20 pointer-events-none">

View File

@ -10,7 +10,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro";
import AllCustomerList from "../../components/Manager/AllCustomerList";
---
<Layout title="Customers List | SiliconPin">
<AllCustomerList client:load />
</Layout>

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro";
import HetznerAllList from "../../components/Manager/HetznerList";
---
<Layout title="Customers List | SiliconPin">
<HetznerAllList client:only="react" />
</Layout>

View File

View File

@ -0,0 +1,7 @@
---
import Layout from "../../layouts/Layout.astro";
import ViewAsUser from "../../components/Manager/ViewAsUser";
---
<Layout title="">
<ViewAsUser client:load />
</Layout>

View File

View File

@ -10,7 +10,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">

View File

@ -0,0 +1,7 @@
---
import Layout from "../../layouts/Layout.astro";
import BuyVPN from "../../components/BuyServices/BuyVPN"
---
<Layout title="Buy VPN | WireGuard | SiliconPin">
<BuyVPN client:load />
</Layout>

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro"
import NewHetznerCloudInstance from "../../components/BuyServices/NewHetznerCloudInstance";
---
<Layout title="Cloud Instance | SiliconPin">
<NewHetznerCloudInstance client:load />
</Layout>

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro"
import NewUthoCloudInstance from "../../components/BuyServices/NewUthoCloudInstance";
---
<Layout title="Cloud Instance | SiliconPin">
<NewUthoCloudInstance client:load />
</Layout>

View File

@ -0,0 +1,7 @@
---
import Layout from "../../layouts/Layout.astro"
import NewDroplet from "../../components/BuyServices/NewDroplet";
---
<Layout title="Create Droplets | SiliconPin">
<NewDroplet client:load />
</Layout>

View File

@ -1,74 +1,102 @@
---
import Layout from '../layouts/Layout.astro';
import { ServiceCard } from '../components/ServiceCard';
import Layout from '../../layouts/Layout.astro';
import { ServiceCard } from '../../components/ServiceCard';
// Page-specific SEO metadata
const pageTitle = "Hosting Services | SiliconPin";
const pageDescription = "Explore SiliconPin's reliable hosting services for PHP, Node.js, Python, Kubernetes (K8s), and K3s. Scalable solutions for businesses of all sizes with 24/7 support.";
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
// Service data
// Service
const services = [
{
title: 'VPN (WireGuard)',
description: 'WireGuard is much better than other solutions like open VPN Free VPN. as WireGuard is the best we provide WireGuard VPN',
imageUrl: '/assets/images/wiregurd-thumb.jpg',
features: ["600 GB bandwidth", "WireGuard® protocol", "Global server locations", "No activity logs"],
learnMoreUrl: '/services/vpn',
buyButtonText: 'Buy VPN',
buyButtonUrl: '/services/buy-vpn'
},{
title: 'Deploy an App',
description: 'WordPress, Joomla, Drupal, PrestaShop, Wiki, Moodle, Directus, PocketBase, StarAPI and more.',
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
imageUrl: '/assets/images/deployapp-thumb.jpg',
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
learnMoreUrl: '/services/deploy-an-app'
learnMoreUrl: '/services/deploy-an-app',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: 'Deploy From Source Code',
description: 'Node.js, Python, Ruby, Go, Rust, and more. Deploy your custom applications with ease.',
imageUrl: 'https://images.unsplash.com/photo-1599507593499-a3f7d7d97667?q=80&w=2000&auto=format&fit=crop',
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
learnMoreUrl: '/services/deploy-from-source-code'
learnMoreUrl: '/services/deploy-from-source-code',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: 'Static Site Hosting',
description: 'Secure and scalable hosting for static websites and JAMstack applications.',
imageUrl: 'https://images.unsplash.com/photo-1526379879527-8559ecfcaec0?q=80&w=2000&auto=format&fit=crop',
features: ['JAMstack', 'Gatsby', 'Hugo', 'Next.js', 'Nuxt.js', 'VuePress', 'Eleventy', 'SvelteKit', 'Astro'],
learnMoreUrl: '/services/static-site-hosting'
learnMoreUrl: '/services/static-site-hosting',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: "Python Hosting",
description: "High-performance hosting for Python applications with support for all major frameworks.",
imageUrl: "https://images.unsplash.com/photo-1526379095098-d400fd0bf935?q=80&w=2000&auto=format&fit=crop",
features: ["Django", "Flask", "FastAPI", "Pyramid", "Bottle", "WSGI Support", "ASGI Support", "Celery", "Redis"],
learnMoreUrl: "/services/python-hosting",
buyButtonText: "",
buyButtonUrl: ""
},
{
title: "Node.js Hosting",
description: "Optimized cloud hosting for Node.js applications with automatic scaling.",
imageUrl: "/assets/node-js.jpg",
features: ["Express", "NestJS", "Koa", "Socket.io", "PM2", "WebSockets", "Serverless", "TypeScript", "GraphQL"],
learnMoreUrl: "/services/nodejs-hosting",
buyButtonText: "",
buyButtonUrl: ""
},
{
title: 'Kubernetes (K8s)',
description: 'Enterprise-grade Kubernetes clusters for container orchestration at scale.',
imageUrl: 'https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop',
features: [
'Fully managed K8s clusters',
'Auto-scaling and load balancing',
'Advanced networking options',
'Persistent storage solutions',
'Enterprise support available'
],
learnMoreUrl: '/services/kubernetes'
features: [ 'Fully managed K8s clusters', 'Auto-scaling and load balancing', 'Advanced networking options', 'Persistent storage solutions', 'Enterprise support available'],
learnMoreUrl: '/services/kubernetes',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: 'K3s Lightweight Kubernetes',
description: 'Lightweight Kubernetes for edge, IoT, and resource-constrained environments.',
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
features: [
'Minimal resource requirements',
'Single binary < 100MB',
'Edge computing optimized',
'ARM support (Raspberry Pi compatible)',
'Simplified management'
],
learnMoreUrl: '/services/k3s'
features: [ 'Minimal resource requirements', 'Single binary < 100MB', 'Edge computing optimized', 'ARM support (Raspberry Pi compatible)', 'Simplified management'],
learnMoreUrl: '/services/k3s',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: 'Hire a Human Developer',
description: 'Need a custom solution? Our experts can design a tailored App or WebApp for your specific needs.',
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
features: ['Node.js', 'Python', 'Ruby', 'Go', 'Rust', 'Docker', 'Kubernetes', 'JAMstack', 'Serverless'],
learnMoreUrl: '/services/hire-a-human-developer'
learnMoreUrl: '/services/hire-a-human-developer',
buyButtonText: '',
buyButtonUrl: ''
},
{
title: 'Hire an AI Agent',
description: 'Need a custom solution? Our experts can design a tailored AI Agent for your specific needs.',
imageUrl: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=2000&auto=format&fit=crop',
features: ['Reactive Agents (Stateless, Rule-Based)', 'Proactive Agents (Stateful, Machine Learning)', 'Hybrid Agents (Reactive + Proactive)', 'Model-Based Agents (Stateful, Uses Memory)', 'Goal-Based Agents (Optimizes for an Objective)', 'Utility-Based Agents', 'Self Learning Agents', 'Autonomous AI Agents'],
learnMoreUrl: '/services/hire-an-ai-agent'
learnMoreUrl: '/services/hire-an-ai-agent',
buyButtonText: '',
buyButtonUrl: ''
}
];
---
@ -76,7 +104,7 @@ const services = [
<Layout
title={pageTitle}
description={pageDescription}
image={pageImage}
ogImage={pageImage}
type="website"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
@ -91,14 +119,16 @@ const services = [
<h2 id="services-heading" class="sr-only">Our hosting services</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8">
{services.map((service, index) => (
<ServiceCard
client:load
title={service.title}
description={service.description}
imageUrl={service.imageUrl}
features={service.features}
learnMoreUrl={service.learnMoreUrl}
/>
<ServiceCard
client:load
title={service.title}
description={service.description}
imageUrl={service.imageUrl}
features={service.features}
learnMoreUrl={service.learnMoreUrl}
buyButtonText={service.buyButtonText}
buyButtonUrl={service.buyButtonUrl}
/>
))}
</div>
</section>
@ -118,8 +148,7 @@ const services = [
</a>
<a
href="/services/custom"
class="inline-flex items-center justify-center rounded-md border border-[#6d9e37] px-6 py-3 text-base font-medium text-[#6d9e37] hover:bg-[#6d9e37] hover:text-white transition-colors w-full sm:w-auto"
>
class="inline-flex items-center justify-center rounded-md border border-[#6d9e37] px-6 py-3 text-base font-medium text-[#6d9e37] hover:bg-[#6d9e37] hover:text-white transition-colors w-full sm:w-auto">
Learn About Custom Solutions
</a>
</div>

8
src/pages/sign-up.astro Normal file
View File

@ -0,0 +1,8 @@
---
import Layout from "../layouts/Layout.astro"
import SignupPage from "../components/SignupPage";
---
<Layout title="">
<SignupPage client:load />
</Layout>

View File

@ -38,16 +38,17 @@ export async function getStaticPaths() {
params: { id: topic.slug },
}));
console.log('Generated Static Paths:', paths);
// console.log('Generated Static Paths:', paths);
return paths;
} catch (error) {
console.error('Error generating static paths:', error);
return [];
}
}
// console.log(topic)
---
<Layout title={topic?.title ?? 'Topic'}>
<Layout title={topic?.title ?? 'Topic | SiliconPin'} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
<TopicDetail client:load topic={topic} />
</Layout>

1163
yarn.lock

File diff suppressed because it is too large Load Diff