Compare commits

...

7 Commits

Author SHA1 Message Date
suvodip ghosh 1c6e06bd6d s1 2025-06-18 08:44:08 +00:00
suvodip ghosh f6857712a8 fix markdown editor css and change mobile bottom nav menu 2025-06-16 15:42:05 +00:00
suvodip ghosh 22fade091d s1 2025-06-12 06:42:01 +00:00
suvodip ghosh b8e20ab510 add text to audio 2025-06-06 08:34:20 +00:00
suvodip ghosh 6071cd5228 update feat 2025-06-06 05:29:39 +00:00
rajib 98a17011a2 Merge pull request 'Update src/pages/contact.astro' (#33) from copy_main into main
Reviewed-on: #33
2025-05-27 07:19:26 +00:00
Subhodip Ghosh b066a53ffa Merge pull request 'Update src/pages/contact.astro' (#32) from copy_main into main
Reviewed-on: #32
2025-05-27 06:42:57 +00:00
46 changed files with 5198 additions and 261 deletions

10
package-lock.json generated
View File

@ -25,6 +25,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"image-resize-compress": "^2.1.1",
"lucide-react": "^0.484.0",
"marked": "^15.0.8",
"menubar": "^9.5.1",
@ -7737,6 +7738,15 @@
],
"license": "BSD-3-Clause"
},
"node_modules/image-resize-compress": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/image-resize-compress/-/image-resize-compress-2.1.1.tgz",
"integrity": "sha512-aF4O1ZzAM0wDziIQZdqlxvDBe163J1Kiu+hSXbYNaGDcD4ggqAgRLLtRmpspnpJPHa4PkhVjwOshzBMHbOWESQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",

View File

@ -27,6 +27,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"image-resize-compress": "^2.1.1",
"lucide-react": "^0.484.0",
"marked": "^15.0.8",
"menubar": "^9.5.1",
@ -48,4 +49,4 @@
"xlsx": "^0.18.5"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View File

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

12
public/assets/js/index.min.js vendored Normal file
View File

@ -0,0 +1,12 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/image-resize-compress@2.1.1/dist/index.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
var h=e=>({png:"image/png",webp:"image/webp",bmp:"image/bmp",gif:"image/gif",jpeg:"image/jpeg"})[e]||"image/jpeg",w=(e,t="auto",r="auto")=>{let o=t==="auto"||t===0,m=r==="auto"||r===0;if(!o&&!m)return{width:t,height:r};if(!o){let n=e.naturalWidth/t;return{width:t,height:Math.round((e.naturalHeight/n+Number.EPSILON)*100)/100}}if(!m){let n=e.naturalHeight/r;return{width:Math.round((e.naturalWidth/n+Number.EPSILON)*100)/100,height:r}}return{width:e.naturalWidth,height:e.naturalHeight}},E=async(e,t=100,r="auto",o="auto",m=null,n=null)=>{if(!(e instanceof Blob))throw new TypeError(`Expected a Blob or File, but got ${typeof e}.`);if(e.size===0)throw new Error("Failed to load the image. The file might be corrupt or empty.");if(t<=0)throw new RangeError("Quality must be greater than 0.");if(typeof r=="number"&&r<0||typeof o=="number"&&o<0)throw new RangeError("Invalid width or height value!");let a=m?h(m):e.type,g=t<1?t:t/100;return new Promise((p,l)=>{let u=new FileReader;u.onload=()=>{let s=new Image;s.src=u.result,s.onload=()=>{let i=document.createElement("canvas"),d=w(s,r,o);i.width=d.width,i.height=d.height;let f=i.getContext("2d");if(!f){l(new Error("Failed to get canvas context."));return}n&&a==="image/png"&&(f.fillStyle=n,f.fillRect(0,0,i.width,i.height)),f.drawImage(s,0,0,i.width,i.height),i.toBlob(b=>{if(b===null)return l(new Error("Failed to generate image blob."));p(new Blob([b],{type:a}))},a,g)},s.onerror=()=>{l(new Error("Failed to load the image. The file might be corrupt or empty."))}},u.onerror=()=>l(new Error("Failed to read the blob as a Data URL.")),u.readAsDataURL(e)})},c=E;var F=async(e,t=100,r="auto",o="auto",m,n)=>{try{let a=await fetch(e,n);if(!a.ok)throw new Error(`Failed to fetch image: ${a.statusText}`);let g=await a.blob();return await c(g,t,r,o,m)}catch(a){throw new Error(`Failed to process the image from URL. Check CORS or network issues. Error: ${a}`)}},y=F;var I=e=>new Promise((t,r)=>{if(e.size===0)return r(new Error("Cannot convert empty Blob."));if(e.size>10485760)return r(new Error("File size exceeds the maximum allowed limit."));let o=new FileReader;o.onloadend=()=>{o.result?t(o.result):r(new Error("Failed to convert blob to DataURL."))},o.onerror=()=>r(new Error("Error reading blob.")),o.readAsDataURL(e)}),R=I;var L=async(e,t)=>{try{let r=await fetch(e,t);if(!r.ok)throw new Error(`Failed to fetch image: ${r.statusText}`);return await r.blob()}catch(r){throw new Error(`Failed to fetch image from URL. Error: ${r}`)}},T=L;export{R as blobToURL,c as fromBlob,y as fromURL,T as urlToBlob};
/*!
* image-resize-compress
* Copyright(c) 2024 Álef Duarte
* MIT Licensed
*/

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,7 @@ export default function BuyVPN() {
setError('Please select a billing cycle');
return;
}
setIsProcessing(true);
setError(null);
@ -146,7 +146,7 @@ export default function BuyVPN() {
</label>
</div>
)
}
}
{/* Selection Summary */}
{selectedCycle && (

View File

@ -0,0 +1,486 @@
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 { Switch } from "../ui/switch";
export default function NewKubernetesService() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [vpcs, setVpcs] = useState([]);
const [subnets, setSubnets] = useState([]);
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
const [formData, setFormData] = useState({
dcslug: "innoida",
cluster_version: "1.24",
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
firewall: "",
vpc: "",
subnet: "",
network_type: "public",
cpumodel: "intel"
});
useEffect(() => {
const fetchInitialData = async () => {
setIsFetchingData(true);
try {
const [plansResponse, vpcsResponse] = await Promise.all([
fetch("https://api.utho.com/v2/plans", {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
}),
fetch("https://api.utho.com/v2/vpc", {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
})
]);
const plansData = await plansResponse.json();
const vpcsData = await vpcsResponse.json();
setPlans(plansData.plans || []);
setVpcs(vpcsData.vpc || []);
// Set default VPC if available
if (vpcsData.vpc?.length > 0) {
const firstVpc = vpcsData.vpc[0];
setFormData(prev => ({
...prev,
vpc: firstVpc.id
}));
// Fetch subnets for the default VPC
await fetchSubnets(firstVpc.id);
}
} catch (error) {
console.error("Initial data fetch error:", error);
showToast({
title: "Error",
description: "Failed to load initial configuration data",
variant: "destructive"
});
} finally {
setIsFetchingData(false);
}
};
fetchInitialData();
}, []);
const fetchSubnets = async (vpcId) => {
try {
// First try to get subnets from the VPC data
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
if (selectedVpc?.subnet?.length > 0) {
setSubnets(selectedVpc.subnet);
setFormData(prev => ({
...prev,
subnet: selectedVpc.subnet[0].id
}));
return;
}
// If no subnets in VPC data, try the subnets endpoint
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
}
});
if (response.ok) {
const data = await response.json();
setSubnets(data.subnets || []);
if (data.subnets?.length > 0) {
setFormData(prev => ({
...prev,
subnet: data.subnets[0].id
}));
}
}
} catch (error) {
console.error("Error fetching subnets:", error);
}
};
const handleVpcChange = async (vpcId) => {
setFormData(prev => ({
...prev,
vpc: vpcId,
subnet: ""
}));
await fetchSubnets(vpcId);
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
try {
// Validate required fields including subnet
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
throw new Error("Please fill all required fields including VPC and Subnet");
}
const payload = {
dcslug: formData.dcslug,
cluster_version: formData.cluster_version,
cluster_label: formData.cluster_label,
nodepools: formData.nodepools.map(pool => ({
label: pool.label,
size: pool.size,
count: pool.count.toString(),
maxCount: pool.maxCount.toString()
})),
firewall: formData.firewall || undefined,
vpc: formData.vpc,
subnet: formData.subnet, // Now required based on API behavior
network_type: formData.network_type,
cpumodel: formData.cpumodel
};
console.log("Deployment payload:", payload);
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
method: "POST",
headers: {
"Authorization": UTHO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
if (data.status === "success") {
showToast({
title: "Success",
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
variant: "success"
});
// Reset form
setFormData(prev => ({
...prev,
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
vpc: vpcs[0]?.id || "",
subnet: subnets[0]?.id || ""
}));
} else {
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
console.error("Deployment error:", error);
} finally {
setIsLoading(false);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleNodePoolChange = (index, field, value) => {
const updatedNodePools = [...formData.nodepools];
updatedNodePools[index][field] = value;
setFormData(prev => ({
...prev,
nodepools: updatedNodePools
}));
};
if (isFetchingData) {
return (
<div className="flex items-center justify-center h-64">
<Loader />
<span className="ml-2">Loading configuration...</span>
</div>
);
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
<CardDescription>
Configure your Kubernetes cluster with the required parameters
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Cluster Label */}
<div className="space-y-2">
<Label htmlFor="cluster_label">Cluster Label *</Label>
<Input
id="cluster_label"
name="cluster_label"
value={formData.cluster_label}
onChange={handleChange}
placeholder="my-cluster"
required
/>
</div>
{/* Data Center and Version */}
<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})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="innoida">Noida</SelectItem>
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
<SelectItem value="indelhi">Delhi</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="cluster_version"
value={formData.cluster_version}
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1.24">1.24</SelectItem>
<SelectItem value="1.25">1.25</SelectItem>
<SelectItem value="1.26">1.26</SelectItem>
<SelectItem value="1.27">1.27</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Network Type and CPU Model */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="network_type">Network Type *</Label>
<Select
name="network_type"
value={formData.network_type}
onValueChange={(value) => setFormData({...formData, network_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select network type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="public">Public</SelectItem>
<SelectItem value="private">Private</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cpumodel">CPU Model *</Label>
<Select
name="cpumodel"
value={formData.cpumodel}
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select CPU model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="intel">Intel</SelectItem>
<SelectItem value="amd">AMD</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Node Pool Configuration */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
{formData.nodepools.map((pool, index) => (
<div key={index} className="p-4 border rounded-lg space-y-4">
<div className="space-y-2">
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
<Input
id={`pool-label-${index}`}
value={pool.label}
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
<Select
value={pool.size}
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
required
>
<SelectTrigger>
<SelectValue placeholder="Select node size" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
<Input
id={`pool-count-${index}`}
type="number"
min="1"
value={pool.count}
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
<Input
id={`pool-maxCount-${index}`}
type="number"
min={pool.count}
value={pool.maxCount}
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
/>
</div>
</div>
</div>
))}
</div>
{/* VPC and Subnet */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="vpc">VPC *</Label>
<Select
name="vpc"
value={formData.vpc}
onValueChange={handleVpcChange}
required
>
<SelectTrigger>
<SelectValue placeholder="Select VPC" />
</SelectTrigger>
<SelectContent>
{vpcs.map(vpc => (
<SelectItem key={vpc.id} value={vpc.id}>
{vpc.name || vpc.id}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="subnet">Subnet *</Label>
<Select
name="subnet"
value={formData.subnet}
onValueChange={(value) => setFormData({...formData, subnet: value})}
required
disabled={!formData.vpc || subnets.length === 0}
>
<SelectTrigger>
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{subnets.map(subnet => (
<SelectItem key={subnet.id} value={subnet.id}>
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
</SelectItem>
))}
</SelectContent>
</Select>
{subnets.length === 0 && formData.vpc && (
<p className="text-sm text-muted-foreground mt-1">
No subnets found in this VPC. Please create a subnet first.
</p>
)}
</div>
</div>
{/* Firewall (Optional) */}
<div className="space-y-2">
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
<Input
id="firewall"
name="firewall"
value={formData.firewall}
onChange={handleChange}
placeholder="Firewall ID"
/>
</div>
<div className="flex justify-end pt-4">
<Button
type="submit"
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
className="w-full md:w-auto"
>
{isLoading ? (
<>
<Loader className="mr-2 h-4 w-4" />
Deploying...
</>
) : "Deploy Kubernetes Cluster"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,114 @@
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 { Loader2 } from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
const PRICE_CONFIG = [
{purchaseType: 'loose', price: 2000, minute: 10000},
{purchaseType: 'bulk', price: 1000, minute: 10000}
];
export default function STTStreaming(){
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const [purchaseType, setPurchaseType] = useState();
const [amount, setAmount] = useState();
const [dataError, setDataError] = useState();
const [isProcessing, setIsProcessing] = useState(false);
const handlePurchaseType = (type) => {
const selected = PRICE_CONFIG.find(item => item.purchaseType === type);
setAmount(type === 'bulk' ? selected.price : type === 'loose' ? selected.price : '')
setPurchaseType(type);
}
const handlePurchase = async () => {
// Validate inputs
if (!purchaseType) {
setDataError('Please select a Plan');
return;
}
setIsProcessing(true);
setDataError(null);
try {
const formData = new FormData();
formData.append('service', 'STT Streaming API');
formData.append('serviceId', 'sttstreamingapi');
formData.append('cycle', 'monthly');
formData.append('amount', amount.toString());
formData.append('service_type', 'streaming_api');
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=streaming_api&orderId=${data.order_id}`;
} else {
throw new Error(data.message || 'Payment initialization failed');
}
} catch (error) {
console.error('Purchase error:', error);
setDataError(error.message || 'An error occurred during purchase. Please try again.');
} finally {
setIsProcessing(false);
}
};
if(!purchaseType){
<div>
<p>{dataError}</p>
</div>
}
return(
<>
<div className='container mx-auto'>
<Card className='max-w-2xl mx-auto px-4 mt-8'>
<CardContent>
<CardHeader>
<CardTitle>STT Streaming API</CardTitle>
<CardDescription>Real-time Speech-to-Text (STT) streaming that converts spoken words into accurate, readable text</CardDescription>
</CardHeader>
<div className='flex flex-row justify-between items-center gap-x-6'>
<label htmlFor="purchase-type-loose" className={`border ${purchaseType === 'loose' ? '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 onChange={() => handlePurchaseType('loose')} type="radio" name="purchase-type" id="purchase-type-loose" className='hidden' />
<div className='flex flex-col'>
<span className='text-2xl font-bold'>Loose Plan</span>
<span className=''>&#x1F550;10,000 Min &#8377;2000</span>
</div>
</label>
<label htmlFor="purchase-type-bulk" className={`border ${purchaseType === 'bulk' ? '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 onChange={() => handlePurchaseType('bulk')} type="radio" name="purchase-type" id="purchase-type-bulk" className='hidden' />
<div className='flex flex-col'>
<span className='text-2xl font-bold'>Bulk Plan</span>
<span className=''>&#x1F550;10,000 Min &#8377;1000</span>
</div>
</label>
</div>
<ul className="grid grid-cols-2 justify-between mt-2 gap-x-2 text-xs">
{['Setup Charge 5000', '10000 Min INR 2000 in Loose Plan', '10000 Min INR 1000 in Bulk Plan', 'Real-time transcription with high accuracy', 'Supports multiple languages', 'Custom vocabulary for domain-specific terms', 'Integrates easily with video/audio streams', 'Secure and scalable API access', 'Live subtitle overlay options'].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 my-8 w-full'>
<Button onClick={handlePurchase} disabled={!purchaseType} className={`w-full ${!purchaseType ? 'cursor-not-allowed' : ''}`}>{isProcessing ? (<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Processing...</>) : 'Proceed'}</Button>
</div>
</CardContent>
</Card>
</div>
</>
)
}

View File

@ -0,0 +1,371 @@
import React, { useEffect, useState } from 'react';
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Card } from "./ui/card";
import { Label } from "./ui/label";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
import { Input } from "./ui/input";
import { useIsLoggedIn } from '../lib/isLoggedIn';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
export default function Comment(props) {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState({ comment: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState('');
const [isLoadingComments, setIsLoadingComments] = useState(true);
const [editorMode, setEditorMode] = useState('edit');
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [uploadProgress, setUploadProgress] = useState(0);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
// Custom image command for MDEditor
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Load comments when component mounts
useEffect(() => {
if (props.topicId) {
fetchComments(props.topicId);
}
}, [props.topicId]);
const fetchComments = async (topicId) => {
setIsLoadingComments(true);
try {
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch comments');
}
if (data.success && data.comments) {
setComments(data.comments);
} else {
throw new Error('Invalid response format');
}
} catch (error) {
console.error('Error fetching comments:', error);
setSubmitError('Failed to load comments');
} finally {
setIsLoadingComments(false);
}
};
// Upload file to MinIO
const uploadToMinIO = async (file) => {
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
return data.publicUrl;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// Handle image URL insertion
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
setNewComment(prev => ({
...prev,
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
}));
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
// Create preview
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile);
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
setNewComment(prev => ({
...prev,
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
}));
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setSubmitError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
const handleSubmitComment = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError('');
try {
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newComment,
topicId: props.topicId
}),
});
if (response.ok) {
setNewComment({ comment: '' });
setSubmitSuccess(true);
setTimeout(() => setSubmitSuccess(false), 3000);
// Refresh comments after successful submission
await fetchComments(props.topicId);
} else {
const errorData = await response.json();
setSubmitError(errorData.message || 'Failed to submit comment');
}
} catch (error) {
setSubmitError('Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getUserInitials = (name) => {
if (!name) return 'U';
const words = name.trim().split(' ');
return words
.slice(0, 2)
.map(word => word[0].toUpperCase())
.join('');
};
return (
<>
{/* Comments List */}
<div className="space-y-6 border-t">
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
{isLoadingComments ? (
<div className="flex justify-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div>
</div>
) : comments.length === 0 ? (
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
) : (
comments.map(comment => (
<div key={comment.id} className="border-b pb-6 last:border-b-0">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
{getUserInitials(comment.userName)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-[#6d9e37]">
{comment.userName || 'User'}
</h4>
<span className="text-xs text-gray-500">
{new Date(comment.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
</div>
<div data-color-mode="light" className="markdown-body">
<MDEditor.Markdown source={comment.comment} />
</div>
</div>
</div>
</div>
))
)}
</div>
{/* Comments Section with MDEditor */}
<Card className="mt-16 border-t">
<div className="relative">
<div className="px-6 pb-6 rounded-lg">
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
{submitSuccess && (
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
Thank you for your comment!
</div>
)}
{submitError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
{submitError}
</div>
)}
<form onSubmit={handleSubmitComment}>
<div className="mb-4">
<Label htmlFor="comment">Comment *</Label>
<div data-color-mode="light">
<MDEditor
placeholder="Write your comment (markdown supported)"
value={newComment.comment}
onChange={(value) => setNewComment(prev => ({ ...prev, comment: value || '' }))}
height={300}
preview={editorMode}
commands={allCommands}
/>
</div>
<div className="flex justify-end mt-2">
<button
type="button"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`text-sm ${editorMode !== 'edit' ? 'bg-[#6d9e37] text-white' : 'text-[#6d9e37]'} px-2 py-1 rounded-md border border-[#6d9e37]`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
</div>
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
{isSubmitting ? 'Submitting...' : 'Post Comment'}
</Button>
</form>
</div>
{/* Image Upload Dialog */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Insert Image</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="image-url">From URL</Label>
<Input
id="image-url"
type="text"
placeholder="Enter image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
className="mt-2"
>
Insert Image
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="image-upload">Upload Image</Label>
<Input
id="image-upload"
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
className="mt-2"
>
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{loading ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
<span className="sr-only">Loading...</span>
</div>
</>
) : !isLoggedIn ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Join the conversation! Log in or sign up to post a comment
</p>
</>
) : null}
</div>
</Card>
</>
);
}

200
src/components/Comment.jsx Normal file
View File

@ -0,0 +1,200 @@
import React, { useEffect, useState } from 'react';
import { Card } from "./ui/card";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
export default function Comment(props) {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState({ comment: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [submitError, setSubmitError] = useState('');
const [isLoadingComments, setIsLoadingComments] = useState(true);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
// Load comments when component mounts or when topicId changes
useEffect(() => {
if (props.topicId) {
fetchComments(props.topicId);
}
}, [props.topicId]);
const fetchComments = async (topicId) => {
setIsLoadingComments(true);
try {
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Failed to fetch comments');
}
if (data.success && data.comments) {
setComments(data.comments);
} else {
throw new Error('Invalid response format');
}
} catch (error) {
console.error('Error fetching comments:', error);
setSubmitError('Failed to load comments');
} finally {
setIsLoadingComments(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewComment(prev => ({
...prev,
[name]: value
}));
};
const handleSubmitComment = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitError('');
try {
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newComment,
topicId: props.topicId
}),
});
if (response.ok) {
setNewComment({ comment: '' });
setSubmitSuccess(true);
setTimeout(() => setSubmitSuccess(false), 3000);
// Refresh comments after successful submission
await fetchComments(props.topicId);
} else {
const errorData = await response.json();
setSubmitError(errorData.message || 'Failed to submit comment');
}
} catch (error) {
setSubmitError('Network error. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const getUserInitials = (name) => {
if (!name) return 'U';
const words = name.trim().split(' ');
return words
.slice(0, 2)
.map(word => word[0].toUpperCase())
.join('');
};
return (
<>
{/* Comments List */}
<div className="space-y-6 border-t">
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
{isLoadingComments ? (
<div className="flex justify-center py-4">
{/* <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div> */}
</div>
) : comments.length === 0 ? (
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
) : (
comments.map(comment => (
<div key={comment.id} className="border-b pb-6 last:border-b-0">
<div className="flex items-start gap-4">
<div className="flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
{getUserInitials(comment.userName)}
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-[#6d9e37]">
{comment.userName || 'User'}
</h4>
<span className="text-xs text-gray-500">
{new Date(comment.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
</div>
<p className="">{comment.comment}</p>
</div>
</div>
</div>
))
)}
</div>
{/* Comments Section */}
<Card className="mt-16 border-t">
<div className="relative">
<div className="px-6 pb-6 rounded-lg">
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
{submitSuccess && (
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
Thank you for your comment!
</div>
)}
{submitError && (
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
{submitError}
</div>
)}
<form onSubmit={handleSubmitComment}>
<div className="mb-4">
<Label htmlFor="comment">Comment *</Label>
<Textarea
className="mt-2"
id="comment"
name="comment"
rows="4"
value={newComment.comment}
onChange={handleInputChange}
required
disabled={!isLoggedIn}
/>
</div>
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
{isSubmitting ? 'Submitting...' : 'Post Comment'}
</Button>
</form>
</div>
{loading ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
<span className="sr-only">Loading...</span>
</div>
</>
) : !isLoggedIn ? (
<>
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Join the conversation! Log in or sign up to post a comment
</p>
</>
) : null}
</div>
</Card>
</>
);
}

View File

@ -4,58 +4,62 @@ import { Textarea } from './ui/textarea';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
import { Button } from './ui/button';
export function ContactForm() {
const [formState, setFormState] = useState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
const [formState, setFormState] = useState({ name: '', email: '', company: '', service: '', message: '' });
const [formStatus, setFormStatus] = useState('idle');
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const [formStatus, setFormStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormState(prev => ({ ...prev, [name]: value }));
};
const handleSelectChange = (value: string) => {
const handleSelectChange = (value) => {
setFormState(prev => ({ ...prev, service: value }));
console.log(formState)
// console.log(formState)
};
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e) => {
e.preventDefault();
setFormStatus('submitting');
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('Form submitted:', formState);
setFormStatus('success');
const payload = {
name: formState.name,
email: formState.email,
company: formState.company,
service_intrest: formState.service,
message: formState.message
};
// Reset form
setFormState({
name: '',
email: '',
company: '',
service: '',
message: '',
});
const response = await fetch(`${USER_API_URL}?query=contact-form`, {
method: "POST",
credentials: "include",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
setTimeout(() => setFormStatus('idle'), 3000);
const data = await response.json();
// console.log('Form Response', data);
if(data.success) {
setFormStatus('success');
// Reset form only after successful submission
setFormState({ name: '', email: '', company: '', service: '', message: '' });
} else {
setFormStatus('error');
console.error('Backend error:', data.message);
}
} catch (error) {
console.error('Submission error:', error);
setFormStatus('error');
setTimeout(() => setFormStatus('idle'), 3000);
console.error('Submission error:', error);
setFormStatus('error');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6">
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-6" id='contact-form'>
<div className="space-y-3 sm:space-y-4">
{/* Name and Email */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">

View File

@ -57,6 +57,14 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
hestia: { monthly: 200, yearly: 2000 }
};
const handleDeploymentType = (e) => {
console.log('setDeploymentType ',e.target.value);
setDeploymentType(e.target.value);
if(e.target.value === 'vpn'){
window.location.href = '/services/buy-vpn';
}
}
// Show toast notification
const showToast = useCallback((message) => {
setToast({ visible: true, message });
@ -451,54 +459,54 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
</div>
);
case 'vpn':
return (
<>
<div className="space-y-2">
<label htmlFor="vpn-continent" className="block text-white font-medium">Select Continent</label>
<select
id="vpn-continent"
value={vpnContinent}
onChange={(e) => setVpnContinent(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="">-Select-</option>
<option value="India">India</option>
<option value="America">America</option>
<option value="Europe">Europe</option>
</select>
</div>
// case 'vpn':
// return (
// <>
// <div className="space-y-2">
// <label htmlFor="vpn-continent" className="block text-white font-medium">Select Continent</label>
// <select
// id="vpn-continent"
// value={vpnContinent}
// onChange={(e) => setVpnContinent(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="">-Select-</option>
// <option value="India">India</option>
// <option value="America">America</option>
// <option value="Europe">Europe</option>
// </select>
// </div>
<ul className="flex justify-between text-sm">
{["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>
<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={() => handleBillingCycleChange('monthly', PRICE_CONFIG.vpn.monthly)} className="hidden" />
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.vpn.monthly}</p>
<span>Monthly</span>
</label>
// <ul className="flex justify-between text-sm">
// {["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>
// <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={() => handleBillingCycleChange('monthly', PRICE_CONFIG.vpn.monthly)} className="hidden" />
// <p className='text-3xl font-bold text-center'>{PRICE_CONFIG.vpn.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`}>
<input type="checkbox" checked={selectedCycle === 'yearly'} onChange={() => handleBillingCycleChange('yearly', PRICE_CONFIG.vpn.yearly)} className="hidden" />
<p className='text-3xl font-bold text-center'>{PRICE_CONFIG.vpn.yearly}</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>
// <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={() => handleBillingCycleChange('yearly', PRICE_CONFIG.vpn.yearly)} className="hidden" />
// <p className='text-3xl font-bold text-center'>{PRICE_CONFIG.vpn.yearly}</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>
<Button onClick={() => handlePurchase('vpn')} className='w-full'>Proceed to Pay</Button>
</>
);
// <Button onClick={() => handlePurchase('vpn')} className='w-full'>Proceed to Pay</Button>
// </>
// );
default:
return null;
@ -514,7 +522,7 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
<select
id="deployment-type"
value={deploymentType}
onChange={(e) => setDeploymentType(e.target.value)}
onChange={handleDeploymentType}
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>

View File

@ -123,6 +123,13 @@ const LoginPage = () => {
}
};
// Set Coockie Function
function setCookie(name : string, value : string, daysToExpire : number) {
const date = new Date();
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = `${name}=${value}; ${expires}; path=/`;
}
const handleCreateUserInMongo = async (siliconId: string) => {
try {
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users`, {
@ -184,7 +191,7 @@ const LoginPage = () => {
// Step 3: Redirect to profile if all are ok
window.location.href = '/profile';
setCookie('token', data.userData.accessToken, 7);
return data;
} catch (error: any) {
console.error('Sync Error:', error);

View File

@ -14,7 +14,7 @@ export default function LoginOrProfile({ deviceType }) {
}
{
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>
<svg className="animate-spin flex flex-col items-center justify-center w-[45px] h-[45px] 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>
)
}

View File

@ -0,0 +1,485 @@
import React, { useState, useEffect, useRef } from "react";
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
const NewTopic = () => {
// Form state
const [formData, setFormData] = useState({ status: 'draft', category: '', title: '', content: '', imageUrl: '' });
// UI state
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Upload file to MinIO
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include',
});
if (!response.ok) {
throw new Error('Upload failed');
}
const data = await response.json();
console.log(data)
return data.url;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// // Generate slug from title
// useEffect(() => {
// if (formData.title) {
// const slug = formData.title
// .toLowerCase()
// .replace(/[^\w\s]/g, '')
// .replace(/\s+/g, '-');
// setFormData(prev => ({ ...prev, slug }));
// }
// }, [formData.title]);
const customImageCommand = {
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
icon: (
<svg width="12" height="12" viewBox="0 0 20 20">
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
</svg>
),
execute: () => {
setImageDialogOpen(true);
},
};
// Get all default commands and replace the image command
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Handle image URL insertion
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
// Create preview
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
setUploadProgress(progress);
});
// Insert markdown for the uploaded image
const imgMarkdown = `![Image](${uploadedUrl})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
// Form submission
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
setUploadProgress(0);
try {
// Upload featured image if selected
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
setUploadProgress(progress);
});
}
// Prepare payload
const payload = {
...formData,
imageUrl
};
// Submit to API
const response = await fetch(`${TOPIC_API_URL}?query=create-new-topic`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to create topic');
}
setSuccess(true);
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
setUploadProgress(0);
}
};
// Handle form field changes
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle featured image file selection
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
// Preview image
const reader = new FileReader();
reader.onload = () => {
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
};
reader.readAsDataURL(file);
}
};
// Reset form
const resetForm = () => {
setFormData({
status: 'draft',
category: '',
title: '',
slug: '',
content: '',
imageUrl: ''
});
setImageFile(null);
setSuccess(false);
setError(null);
};
// Success state
if (success) {
return (
<div className="container mx-auto px-4 text-center py-8">
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic created successfully!</h3>
<div className="flex justify-center gap-4">
<Button onClick={resetForm}>Create Another</Button>
<Button variant="outline" onClick={() => window.location.href = '/'}>Return Home</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Create New Topic</h2>
<p className="text-gray-600 mb-6">Start a new discussion in the SiliconPin community</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
name="status"
required
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Published</SelectItem>
<SelectItem value="archived">Archived</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
name="category"
required
value={formData.category}
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
>
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Title and Slug */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
</div>
{/* <div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
</div> */}
{/* Content Editor with Preview Toggle */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="content">Content *</Label>
<button
type="button"
size="sm"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
<div data-color-mode="light">
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>
{/* Image Upload Dialog */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Insert Image</DialogTitle>
</DialogHeader>
<CustomTabs
tabs={[
{
label: "From URL",
value: "url",
content: (
<div className="space-y-4">
<Input
type="text"
placeholder="Enter image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<div className="flex justify-end">
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
>
Insert Image
</Button>
</div>
</div>
)
},
{
label: "Upload",
value: "upload",
content: (
<div className="space-y-4">
<Input
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
>
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
</Button>
</div>
</div>
)
}
]}
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
<Input
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
</div>
<Separator />
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={resetForm}
disabled={isSubmitting}
>
Reset
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-32"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin"></span>
Creating...
</span>
) : 'Create Topic'}
</Button>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">
Error: {error}
</div>
)}
</form>
</div>
</div>
);
};
export default NewTopic;

View File

@ -7,9 +7,8 @@ import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
const NewTopic = () => {
@ -31,14 +30,13 @@ const NewTopic = () => {
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', 'siliconpin-uploads');
formData.append('folder', 'topic-images');
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
try {
const response = await fetch(MINIO_UPLOAD_URL, {
method: 'POST',
body: formData,
credentials: 'include',
credentials: 'include'
});
if (!response.ok) {
@ -46,7 +44,8 @@ const NewTopic = () => {
}
const data = await response.json();
return data.url;
console.log(data.publicUrl)
return data.publicUrl;
} catch (error) {
console.error('Upload error:', error);
throw error;
@ -85,7 +84,7 @@ const NewTopic = () => {
}
return cmd;
});
// Handle image URL insertion
const handleInsertImageUrl = () => {
@ -127,7 +126,7 @@ const NewTopic = () => {
// Upload image file to MinIO and insert into editor
const handleImageUpload = async () => {
if (!imageUploadFile) return;
console.log(imageUploadFile)
try {
setIsSubmitting(true);
setUploadProgress(0);
@ -174,10 +173,11 @@ const NewTopic = () => {
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
console.log('imageUrl', imageUrl)
setUploadProgress(progress);
});
}
// let finalImageUrl = imageUrl.publicUrl;
// Prepare payload
const payload = {
...formData,
@ -328,14 +328,14 @@ const NewTopic = () => {
</button>
</div>
<div data-color-mode="light">
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
<MDEditor
placeholder="Write your content"
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { FileDown } from "lucide-react";
import { useIsLoggedIn } from '../lib/isLoggedIn';
@ -32,7 +33,7 @@ export default function HestiaCredentialsFetcher() {
}, [sessionData]);
const handleSaveDataInMongoDB = async (servicesData, currentBillingId, serviceToEndpoint, siliconId) => {
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? hostings : '';
const query = serviceToEndpoint === 'vpn' ? 'vpns' : serviceToEndpoint === 'hosting' ? 'hostings' : '';
try {
const serviceDataPayload = {
success: servicesData.success,
@ -46,7 +47,7 @@ export default function HestiaCredentialsFetcher() {
billingId: currentBillingId,
status: "active"
}
console.log('serviceDataPayload', serviceDataPayload)
// console.log('serviceDataPayload', serviceDataPayload)
const response = await fetch(`https://hostapi2.cs1.hz.siliconpin.com/api/users/${siliconId}/${query}`, {
method: 'POST',
headers: {
@ -108,7 +109,7 @@ export default function HestiaCredentialsFetcher() {
element.click();
document.body.removeChild(element);
};
console.log('serviceName', serviceName)
if (loading) {
return <Loader />;
}
@ -168,7 +169,7 @@ export default function HestiaCredentialsFetcher() {
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
) : creds && creds.service === 'hestia' && (
) : 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>
@ -186,7 +187,16 @@ export default function HestiaCredentialsFetcher() {
{error && <p className="text-red-600 mt-2">{error}</p>}
</div>
)}
) : serviceName === 'streaming_api' ? (
<Card className='max-w-2xl mx-auto px-4 mt-8'>
<CardContent>
<CardHeader>
<CardTitle>STT Streaming API Purchase Succesfully</CardTitle>
<CardDescription>You can See Info on my Billing Section in Profile</CardDescription>
</CardHeader>
</CardContent>
</Card>
) : ''}
</div>
);
}

View File

@ -2,22 +2,24 @@ 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;
description: string;
imageUrl: string;
features: string[];
features: [];
learnMoreUrl: string;
buyButtonText: string;
buyButtonUrl: string;
}
export function ServiceCard({ title, description, imageUrl, features, learnMoreUrl, buyButtonText, buyButtonUrl }: ServiceCardProps) {
// console.log('checkType', typeof features)
// console.log('checkType', features)
const { isLoggedIn, loading, sessionData } = useIsLoggedIn();
const [showLoginModal, setShowLoginModal] = useState(false);
const parsedFeatures = typeof features === 'string' ? JSON.parse(features) : features;
const handleBuyClick = () => {
if (!isLoggedIn) {
@ -25,6 +27,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
return;
}
window.location.href = buyButtonUrl;
console.log(buyButtonUrl)
};
return (
@ -39,16 +42,20 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
</div>
<CardHeader className="pb-3">
<CardTitle className="text-xl text-[#6d9e37]">{title}</CardTitle>
<CardDescription className="text-neutral-400">{description}</CardDescription>
<CardDescription className="text-neutral-400 text-justify">{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>
))}
{
parsedFeatures && (
parsedFeatures.map((feature : string, index: number) => (
<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>
@ -60,7 +67,7 @@ export function ServiceCard({ title, description, imageUrl, features, learnMoreU
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
Details
</a>
</div>
</CardFooter>

View File

@ -0,0 +1,132 @@
import React, { useState, useEffect } from "react";
import { fromBlob, blobToURL } from "image-resize-compress";
import { Button } from "../ui/button";
import {
ImageIcon,
DownloadIcon,
SlidersHorizontal,
FileImage,
} from "lucide-react";
export default function ImageResize() {
const [previewUrl, setPreviewUrl] = useState(null);
const [resizedBlob, setResizedBlob] = useState(null);
const [imgQuality, setImgQuality] = useState(80);
const [selectedFile, setSelectedFile] = useState(null);
const [originalSize, setOriginalSize] = useState(0);
const [resizedSize, setResizedSize] = useState(0);
const [originalExtension, setOriginalExtension] = useState("webp");
useEffect(() => {
const resizeImage = async () => {
if (!selectedFile) return;
try {
const width = "auto";
const height = "auto";
const ext = selectedFile.name.split(".").pop().toLowerCase();
setOriginalExtension(ext);
setOriginalSize((selectedFile.size / 1024).toFixed(2)); // KB
// Only apply quality change for JPEG or WebP
const format = ["jpg", "jpeg", "webp"].includes(ext)
? ext
: "jpeg"; // fallback to jpeg for better quality control
const resized = await fromBlob(selectedFile, imgQuality, width, height, format);
const url = await blobToURL(resized);
setResizedBlob(resized);
setResizedSize((resized.size / 1024).toFixed(2)); // KB
setPreviewUrl(url);
} catch (err) {
console.error("Image resizing failed:", err);
}
};
resizeImage();
}, [selectedFile, imgQuality]);
const handleFileChange = (event) => {
const file = event.target.files[0];
if (file) {
setSelectedFile(file);
}
};
const handleDownload = () => {
if (!resizedBlob) return;
const link = document.createElement("a");
link.href = URL.createObjectURL(resizedBlob);
link.download = `resized-image.${originalExtension}`;
link.click();
};
return (
<div className="min-h-screen text-white p-6 flex items-center justify-center">
<div className="bg-gray-800 rounded-2xl shadow-lg w-full max-w-2xl p-6 space-y-6">
<h2 className="text-2xl font-bold flex items-center gap-2 text-[#6d9e37]">
<ImageIcon className="w-6 h-6" />
Image Resizer
</h2>
<label className="flex items-center gap-2 text-sm text-gray-300 font-medium">
<FileImage className="w-4 h-4" />
Choose an image
</label>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded-lg text-sm file:bg-[#6d9e37] file:text-white file:border-0 file:px-3 file:py-1"
/>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium flex items-center gap-1">
<SlidersHorizontal className="w-4 h-4" />
Quality: <span className="text-[#6d9e37]">{imgQuality}%</span>
</label>
<input
type="range"
min="1"
max="100"
value={imgQuality}
onChange={(e) => setImgQuality(e.target.value)}
className="w-full accent-[#6d9e37]"
/>
</div>
{previewUrl && (
<div className="border-t border-gray-700 pt-4 space-y-3">
<div className="text-sm text-gray-300 grid grid-cols-2 gap-2">
<p>
<strong>Original Size:</strong> {originalSize} KB
</p>
<p>
<strong>Resized Size:</strong> {resizedSize} KB
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-400 mb-2">Preview:</p>
<img
src={previewUrl}
alt="Resized Preview"
className="w-full h-auto max-h-96 object-contain rounded-lg border border-gray-700"
/>
</div>
<Button
onClick={handleDownload}
className="mt-4 w-full gap-2"
>
<DownloadIcon className="w-5 h-5" />
Download Resized Image
</Button>
</div>
)}
</div>
</div>
);
}

View File

@ -1,9 +1,15 @@
import React, { useState, useEffect } from "react";
import { marked } from 'marked';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import Comment from './Comment';
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
export default function TopicDetail(props) {
const [showCopied, setShowCopied] = useState(false);
if (!props.topic) {
return <div>Topic not found</div>;
}
@ -12,31 +18,6 @@ export default function TopicDetail(props) {
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':
@ -125,7 +106,7 @@ export default function TopicDetail(props) {
return (
<div className="container mx-auto px-4 py-12">
<article className="max-w-4xl mx-auto">
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
<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 */}
@ -211,11 +192,11 @@ export default function TopicDetail(props) {
</div>
</div>
<div
className="font-light mb-8 text-justify prose max-w-none"
dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}
></div>
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
<Comment topicId={props.topic.id}/>
</article>
</div>
);
}
}
// bg-[#6d9e37]

View File

@ -6,7 +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)
// console.table(props.topics)
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
const [localSearchTerm, setLocalSearchTerm] = React.useState(props.searchTerm || '');

View File

@ -112,10 +112,18 @@ export default function TopicCreation() {
return (
<>
{isLoggedIn && (
<>
<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 className="hidden lg:block" 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>
<button onClick={() => window.location.href = '/topic/new'} className="fixed z-10 bottom-20 lg:bottom-10 right-0 lg:right-10 rounded-full w-16 h-16 bg-[#6d9e37]">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#6d9e37] opacity-75 duration-[1s,15s]"></span>
<span class="relative inline-flex w-16 h-16 rounded-full bg-[#6d9e37]">
<svg className="border-[2px] border-[#fff] rounded-full" fill="#FFFFFF" viewBox="-5.76 -5.76 35.52 35.52" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" d="M12.3023235,7.94519388 L4.69610276,15.549589 C4.29095108,15.9079238 4.04030835,16.4092335 4,16.8678295 L4,20.0029438 L7.06398288,20.004826 C7.5982069,19.9670062 8.09548693,19.7183782 8.49479322,19.2616227 L16.0567001,11.6997158 L12.3023235,7.94519388 Z M13.7167068,6.53115006 L17.4709137,10.2855022 L19.8647941,7.89162181 C19.9513987,7.80501747 20.0000526,7.68755666 20.0000526,7.56507948 C20.0000526,7.4426023 19.9513987,7.32514149 19.8647932,7.23853626 L16.7611243,4.13485646 C16.6754884,4.04854589 16.5589355,4 16.43735,4 C16.3157645,4 16.1992116,4.04854589 16.1135757,4.13485646 L13.7167068,6.53115006 Z M16.43735,2 C17.0920882,2 17.7197259,2.26141978 18.1781068,2.7234227 L21.2790059,5.82432181 C21.7406843,6.28599904 22.0000526,6.91216845 22.0000526,7.56507948 C22.0000526,8.21799052 21.7406843,8.84415992 21.2790068,9.30583626 L9.95750718,20.6237545 C9.25902448,21.4294925 8.26890003,21.9245308 7.1346,22.0023295 L2,22.0023295 L2,21.0023295 L2.00324765,16.7873015 C2.08843822,15.7328366 2.57866679,14.7523321 3.32649633,14.0934196 L14.6953877,2.72462818 C15.1563921,2.2608295 15.7833514,2 16.43735,2 Z"></path> </g></svg>
</span>
</button>
</>
)}
{loading && !topics.length ? (

View File

@ -4,7 +4,6 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "./ui/select";
import { Separator } from "./ui/separator";
import { Textarea } from "./ui/textarea";
import React, { useState, useEffect } from 'react';
@ -266,7 +265,42 @@ export default function ProfilePage() {
</div>
<div className="flex-1 space-y-6 lg:max-w-xl">
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut}/>
<Card className="">
<CardHeader>
<div className="flex flex-row justify-between">
<CardTitle>My Services</CardTitle>
{/* <a href="/profile/billing-info" className="hover:bg-[#6d9e37] hover:text-white transtion duration-500 py-1 px-2 rounded">View All</a> */}
</div>
<CardDescription>
View your all Services.
</CardDescription>
<table className="w-full">
<thead>
<tr>
<th className="text-left">Order ID</th>
<th className="text-center">Date</th>
<th className="text-left">Description</th>
<th className="text-center">Status</th>
{/* <th className="text-center">Action</th> */}
</tr>
</thead>
<tbody>
{
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=""><p className="line-clamp-1">{invoice.service}</p></td>
<td className={`text-center text-sm rounded-full h-fit ${invoice.service_status === '0' ? 'text-yellow-500' : invoice.service_status === '1' ? 'text-green-500' : 'text-red-500'}`}>{invoice.service_status === '1' ? 'Active' : invoice.service_status === '0' ? 'Unactive' : ''}</td>
</tr>
))
}
</tbody>
</table>
</CardHeader>
</Card>
<PasswordUpdateCard userId={userData?.session_data?.id} onLogout={sessionLogOut} />
<Card className="mt-6">
<CardHeader>
<CardTitle>Danger Zone</CardTitle>

View File

@ -2,50 +2,12 @@
import '../styles/global.css';
import LoginProfile from '../components/LoginOrProfile';
interface Props {
title: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
type?: 'website' | 'article';
}
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; newSchema1 : string}
const {
title,
description = "SiliconPin offers high-performance hosting solutions for PHP, Node.js, Python, K8s, and K3s applications with 24/7 support.",
ogImage,
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
type = "website",
} = Astro.props;
const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, type = "website", newSchema1} = Astro.props;
// Organization schema
const organizationSchema = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "SiliconPin",
"url": "https://siliconpin.com",
"logo": "https://siliconpin.com/assets/logo.svg",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+91-700-160-1485",
"contactType": "customer service",
"availableLanguage": ["English", "Hindi", "Bengali"]
},
"address": {
"@type": "PostalAddress",
"streetAddress": "121 Lalbari, GourBongo Road",
"addressLocality": "Habra",
"addressRegion": "W.B.",
"postalCode": "743271",
"addressCountry": "India"
},
"sameAs": [
"https://www.linkedin.com/company/siliconpin",
"https://x.com/dwd_consultancy",
"https://www.facebook.com/profile.php?id=100088549643337",
"https://instagram.com/siliconpin.com_"
]
};
---
<!DOCTYPE html>
@ -83,7 +45,10 @@ const organizationSchema = {
<link rel="apple-touch-icon" href="https://siliconpin.com/assets/logo.svg" />
<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(organizationSchema)}></script>
<script is:inline type="application/ld+json" set:html={newSchema1}></script>
<!-- For markdown editor css -->
<link rel="stylesheet" href="/assets/md-editor/mdeditor.css" />
</head>
<body class="min-h-screen flex flex-col">
<!-- Top header - visible on all devices -->
@ -96,12 +61,12 @@ const organizationSchema = {
<!-- Desktop navigation -->
<nav class="hidden md:flex items-center gap-6" aria-label="Main Navigation">
<a href="/" class="hover:text-[#6d9e37] transition-colors">Home</a>
<a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a>
<!-- <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>
<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>
<a href="/services" 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">Services</a>
</nav>
<!-- Mobile menu button -->
<div class="md:hidden relative">
@ -112,11 +77,11 @@ const organizationSchema = {
<!-- Mobile dropdown menu -->
<div id="mobileMenu" class="absolute right-0 mt-2 w-48 bg-neutral-800 rounded-md shadow-lg py-1 z-50 hidden border border-neutral-700">
<a href="/" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Home</a>
<a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Services</a>
<!-- <a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Services</a> -->
<a href="/topic" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Topic</a>
<a href="/contact" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Contact</a>
<a href="/about-us" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">About Us</a>
<a href="/get-started" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700 font-medium text-[#6d9e37]">Get Started</a>
<a href="/services" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700 font-medium text-[#6d9e37]">Services</a>
<a href="/suggestion-or-report" class="block px-4 py-2 text-sm text-white hover:bg-neutral-700">Suggestion or Report</a>
</div>
</div>
@ -170,8 +135,8 @@ const organizationSchema = {
<div class="bg-neutral-800 p-5 rounded-lg border border-neutral-700 shadow-lg">
<h3 class="font-medium text-lg text-[#6d9e37] mb-4 pb-2 border-b border-[#6d9e37]">Services</h3>
<ul class="space-y-2 text-neutral-400">
<li class="flex items-center gap-2">📦 <a href="/services" class="hover:text-[#6d9e37] transition-colors">All Services</a></li>
<li class="flex items-center gap-2">🚀 <a href="/get-started" class="hover:text-[#6d9e37] transition-colors">Get Started</a></li>
<!-- <li class="flex items-center gap-2">📦 <a href="/services" class="hover:text-[#6d9e37] transition-colors">All Services</a></li> -->
<li class="flex items-center gap-2">🚀 <a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a></li>
<li class="flex items-center gap-2">👨‍💻 <a href="/hire-developer" class="hover:text-[#6d9e37] transition-colors">Hire a Human Developer</a></li>
<li class="flex items-center gap-2">🤖 <a href="/hire-ai-agent" class="hover:text-[#6d9e37] transition-colors">Hire an AI Agent</a></li>
</ul>
@ -222,21 +187,24 @@ const organizationSchema = {
<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"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
<span class="text-xs mt-1">Home</span>
</a>
<a href="/services" 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="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
<span class="text-xs mt-1">Services</span>
<a href="/topic" 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="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> -->
<svg fill="#6d9e37" width="20" height="20" viewBox="-4 -2 24 24" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin" class="jam jam-document-f"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M3 0h10a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm1 7a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H4zm0 8a1 1 0 0 0 0 2h5a1 1 0 0 0 0-2H4zM4 3a1 1 0 1 0 0 2h8a1 1 0 0 0 0-2H4zm0 8a1 1 0 0 0 0 2h8a1 1 0 0 0 0-2H4z"></path></g></svg>
<span class="text-xs mt-1">Topic</span>
</a>
<a href="/get-started" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<a href="/services" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<div class="w-10 h-10 rounded-full bg-[#6d9e37] flex items-center justify-center -mt-5 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
</div>
<span class="text-xs mt-1">Start</span>
<span class="text-xs mt-1">Services</span>
</a>
<a href="/about-us" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<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>
<LoginProfile deviceType="mobile" client:load />
<div class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<LoginProfile deviceType="mobile" client:load />
</div>
<!-- <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>

View File

@ -62,7 +62,7 @@ const contactSchema = {
</div>
<!-- Full width contact form section -->
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
<section aria-labelledby="contact-form-heading" class="mb-8 sm:mb-12 mx-auto bg-neutral-800 rounded-lg p-4 sm:p-6 lg:p-8 border border-neutral-700">
<h2 id="contact-form-heading" class="text-xl sm:text-2xl font-bold text-white mb-4 sm:mb-6">Send us a message</h2>
<ContactForm client:load />
</section>

View File

@ -1,6 +1,34 @@
---
import Layout from '../layouts/Layout.astro';
const schema1 = {
"@context": "https://schema.org",
"@type": "Organization",
"name": "SiliconPin",
"url": "https://siliconpin.com",
"logo": "https://siliconpin.com/assets/logo.svg",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+91-700-160-1485",
"contactType": "customer service",
"availableLanguage": ["English", "Hindi", "Bengali"]
},
"address": {
"@type": "PostalAddress",
"streetAddress": "121 Lalbari, GourBongo Road",
"addressLocality": "Habra",
"addressRegion": "W.B.",
"postalCode": "743271",
"addressCountry": "India"
},
"sameAs": [
"https://www.linkedin.com/company/siliconpin",
"https://x.com/dwd_consultancy",
"https://www.facebook.com/dwdsiliconpin",
"https://instagram.com/siliconpin.com_"
]
};
// 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.";
@ -11,6 +39,8 @@ const pageImage = "/assets/logo.svg";
title={pageTitle}
description={pageDescription}
ogImage={pageImage}
newSchema1={JSON.stringify(schema1)}
>
<!-- Canvas background animation -->
<div class="fixed inset-0 z-0 opacity-20 pointer-events-none">

View File

@ -4,4 +4,4 @@ import BuyVPN from "../../components/BuyServices/BuyVPN"
---
<Layout title="Buy VPN | WireGuard | SiliconPin">
<BuyVPN client:load />
</Layout>
</Layout>

View File

@ -1,111 +1,229 @@
---
// Astro script block
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
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"],
imageUrl: '/assets/images/services/wiregurd-thumb.jpg',
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: '/assets/images/deployapp-thumb.jpg',
features: [ 'WordPress', 'Joomla', 'Drupal', 'PrestaShop', 'Wiki', 'Moodle', 'Directus', 'PocketBase', 'StarAPI' ],
imageUrl: '/assets/images/services/deployapp-thumb.jpg',
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',
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',
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"],
imageUrl: "/assets/images/services/node-js.jpg",
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'],
imageUrl: '/assets/images/services/stt-streaming.png',
learnMoreUrl: '',
buyButtonText: 'Purchase',
buyButtonUrl: '/services/stt-streaming'
},
{
imageUrl: '/assets/images/services/kubernetes-edge.png',
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',
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',
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',
buyButtonText: '',
buyButtonUrl: ''
}
];
const SERVICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/?query=all-services-list';
let data = [];
try {
const response = await fetch(SERVICE_API_URL);
const json = await response.json();
if (json.success) {
// data = [...json.data, ...services];
data = json.data;
} else {
console.error("API error:", json.message);
}
} catch (err) {
console.error("Fetch failed:", err);
}
const servicesData = services.map((item, index) => {
return { ...item, ...data[index] };
});
// SEO
let schema1 = {
"@context": "https://schema.org",
"@type": "Service",
"name": "SiliconPin Hosting & Development Services",
"description": "Reliable, scalable, and secure hosting solutions for all your applications, plus custom developer and AI agent hiring.",
"provider": {
"@type": "Organization",
"name": "SiliconPin",
"url": "https://siliconpin.com",
"telephone": "+91-700-160-1485",
"address": {
"@type": "PostalAddress",
"streetAddress": "121 Lalbari, GourBongo Road",
"addressLocality": "Habra",
"addressRegion": "West Bengal",
"postalCode": "743271",
"addressCountry": "IN"
}
},
"areaServed": "Worldwide",
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "SiliconPin Services",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "VPN (WireGuard)",
"description": "WireGuard VPN with 600GB bandwidth, no logs, global servers."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "App Deployment",
"description": "Deploy WordPress, Joomla, Drupal, Moodle, Directus, PocketBase, StarAPI and more."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Deploy From Source Code",
"description": "Deploy custom Node.js, Python, Ruby, Go, Rust, Docker, Kubernetes, JAMstack & serverless apps."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Static Site Hosting",
"description": "Hosting for JAMstack static sites via Gatsby, Hugo, Next.js, Nuxt.js, VuePress, SvelteKit, Astro."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Python Hosting",
"description": "Highperformance hosting for Django, Flask, FastAPI, Pyramid, Bottle with WSGI/ASGI, Celery, Redis."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Node.js Hosting",
"description": "Optimized cloud hosting of Node.js apps with autoscaling, WebSockets, PM2, GraphQL support."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "STT Streaming (API)",
"description": "Realtime speechtotext streaming with multilanguage support, custom vocabulary, live subtitle overlay."
},
"priceCurrency": "INR",
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": "2000",
"billingIncrement": 10000,
"description": "Loose plan per 10000 minutes"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Kubernetes (K8s)",
"description": "Fully managed enterprisegrade Kubernetes with autoscaling, load balancing, persistent storage."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "K3s Lightweight Kubernetes",
"description": "Lightweight K3s clusters for edge/IoT—<100MB binary, ARM support, simplified management."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Hire a Human Developer",
"description": "Custom app/webapp development using Node.js, Python, Ruby, Go, Rust, Docker, Kubernetes, JAMstack, serverless."
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Hire an AI Agent",
"description": "Build reactive, proactive, hybrid, selflearning or goalbased AI agents tailored to your needs."
}
}
]
}
}
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";
---
<Layout
title={pageTitle}
description={pageDescription}
ogImage={pageImage}
type="website"
newSchema1={JSON.stringify(schema1)}
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">
@ -118,17 +236,17 @@ const services = [
<section aria-labelledby="services-heading">
<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}
buyButtonText={service.buyButtonText}
buyButtonUrl={service.buyButtonUrl}
/>
{servicesData.map((service : any, index: number) => (
<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>

View File

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

View File

@ -0,0 +1,7 @@
---
import Layout from "../../layouts/Layout.astro"
import STTStreaming from "../../components/BuyServices/STTStreaming"
---
<Layout title="STT Streaming | SiliconPin">
<STTStreaming client:load />
</Layout>

100
src/pages/tmp.astro Normal file
View File

@ -0,0 +1,100 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout title="">
<div>
<button id="startBtn">Start Speaking</button>
<button id="stopBtn">Stop</button>
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
</div>
</Layout>
<script is:inline>
/**
* Speech-to-Text function that listens to microphone input and displays transcribed text
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
* @param {function} onResult - Optional callback function that receives the transcribed text
* @returns {object} An object with start and stop methods to control the recognition
*/
function speechToText(outputElement, onResult) {
// Check if browser supports the Web Speech API
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
return;
}
// Create speech recognition object
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
// Configure recognition settings
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US'; // Set language - change as needed
let finalTranscript = '';
// Event handler for recognition results
recognition.onresult = (event) => {
let interimTranscript = '';
// Clear final transcript at the start of new session
if (event.resultIndex === 0) {
finalTranscript = '';
}
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
// Replace rather than append to avoid duplicates
finalTranscript = transcript;
if (onResult) onResult(finalTranscript);
} else {
interimTranscript = transcript;
}
}
// Update the output element
if (outputElement) {
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
}
};
recognition.onerror = (event) => {
console.error('Speech recognition error', event.error);
};
recognition.onend = () => {
console.log('Speech recognition ended');
};
// Return control methods
return {
start: () => recognition.start(),
stop: () => recognition.stop(),
abort: () => recognition.abort(),
getFinalTranscript: () => finalTranscript
};
}
// Get DOM elements
const outputElement = document.getElementById('outputText');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
// Initialize speech recognition
const recognizer = speechToText(outputElement, (finalText) => {
console.log('Final text:', finalText);
});
// Add button event listeners
startBtn.addEventListener('click', () => {
outputElement.textContent = 'Listening...';
recognizer.start();
});
stopBtn.addEventListener('click', () => {
recognizer.stop();
console.log('Final transcript:', recognizer.getFinalTranscript());
});
</script>

View File

@ -19,7 +19,7 @@ try {
// console.log('Topic Data', data)
topics = data.data || [];
} catch (error) {
console.error('An error occurred', error);
console.error('An error occurred', error);
}
---

View File

@ -0,0 +1,200 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div class=" min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
<div class="bg-blue-600 p-4 text-white">
<h1 class="text-2xl font-bold">Real-Time Speech Transcription</h1>
<p class="text-blue-100">Using Annyang.js for speech recognition</p>
</div>
<div class="p-6">
<div class="mb-6">
<div class="flex items-center justify-between mb-2">
<h2 class="text-lg font-semibold text-gray-800">Transcription</h2>
<div id="status" class="px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800">
Not Listening
</div>
</div>
<div id="transcript" class="h-64 p-4 border border-gray-300 rounded-lg overflow-y-auto bg-gray-50">
<p class="text-gray-500 italic">Your transcribed text will appear here...</p>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<button id="startBtn" class="flex-1 bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg transition">
Start Listening
</button>
<button id="stopBtn" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition" disabled>
Stop Listening
</button>
<button id="clearBtn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-4 rounded-lg transition">
Clear Text
</button>
</div>
<div class="mt-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Available Commands</label>
<div class="bg-gray-50 p-3 rounded-lg border border-gray-200">
<ul class="list-disc pl-5 text-sm text-gray-600">
<li>"clear transcript" - Clears the transcription</li>
<li>"new paragraph" - Inserts a new paragraph</li>
<li>"period", "comma", "question mark" - Adds punctuation</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</Layout>
<script is:inline src="https://cdnjs.cloudflare.com/ajax/libs/annyang/2.6.1/annyang.min.js"></script>
<script is:inline>
document.addEventListener('DOMContentLoaded', function() {
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const clearBtn = document.getElementById('clearBtn');
const transcript = document.getElementById('transcript');
const status = document.getElementById('status');
// Check if browser supports speech recognition
if (!annyang) {
status.textContent = "Speech recognition not supported";
startBtn.disabled = true;
return;
}
// Add commands
const commands = {
'*text': function(phrase) {
appendToTranscript(phrase);
},
'clear transcript': function() {
clearTranscript();
},
'new paragraph': function() {
appendToTranscript("\n\n");
},
'period': function() {
appendToTranscript(". ");
},
'comma': function() {
appendToTranscript(", ");
},
'question mark': function() {
appendToTranscript("? ");
},
'exclamation mark': function() {
appendToTranscript("! ");
}
};
annyang.addCommands(commands);
// Event listeners
startBtn.addEventListener('click', function() {
annyang.start({
autoRestart: true,
continuous: true
});
status.textContent = "Listening...";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-green-200 text-green-800";
startBtn.disabled = true;
stopBtn.disabled = false;
});
stopBtn.addEventListener('click', function() {
try {
annyang.abort();
} catch (e) {
console.log("Stopping recognition", e);
}
status.textContent = "Not Listening";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-800";
startBtn.disabled = true;
stopBtn.disabled = true;
setTimeout(() => {
startBtn.disabled = false;
}, 500);
});
clearBtn.addEventListener('click', clearTranscript);
// Callbacks
annyang.addCallback('start', function() {
console.log("Speech recognition started");
});
annyang.addCallback('soundstart', function() {
console.log("User is speaking");
});
annyang.addCallback('end', function() {
console.log("Speech recognition ended");
});
annyang.addCallback('error', function(err) {
if (err.error === 'aborted') {
console.log("Recognition stopped manually");
return;
}
console.error("Error:", err);
status.textContent = "Error occurred";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
startBtn.disabled = false;
stopBtn.disabled = true;
});
annyang.addCallback('errorNetwork', function(err) {
console.error("Network error:", err);
status.textContent = "Network error";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
startBtn.disabled = false;
stopBtn.disabled = true;
});
annyang.addCallback('errorPermissionBlocked', function() {
console.error("Permission blocked");
status.textContent = "Permission blocked";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
startBtn.disabled = true;
stopBtn.disabled = true;
});
annyang.addCallback('errorPermissionDenied', function() {
console.error("Permission denied");
status.textContent = "Permission denied";
status.className = "px-3 py-1 rounded-full text-sm font-medium bg-red-200 text-red-800";
startBtn.disabled = true;
stopBtn.disabled = true;
});
// Helper functions
function appendToTranscript(text) {
if (transcript.innerHTML.includes("Your transcribed text will appear here...")) {
transcript.innerHTML = '';
}
if (text === "\n\n") {
transcript.innerHTML += '<p class="mt-4"></p>';
} else {
let paragraphs = transcript.querySelectorAll('p');
let lastParagraph = paragraphs.length > 0 ? paragraphs[paragraphs.length - 1] : null;
if (!lastParagraph) {
lastParagraph = document.createElement('p');
transcript.appendChild(lastParagraph);
}
lastParagraph.textContent += text;
}
transcript.scrollTop = transcript.scrollHeight;
}
function clearTranscript() {
transcript.innerHTML = '<p class="text-gray-500 italic">Your transcribed text will appear here...</p>';
}
});
</script>

View File

@ -0,0 +1,168 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="w-full max-w-2xl bg-white rounded-lg shadow-lg overflow-hidden">
<!-- Header -->
<div class="bg-blue-600 text-white p-4">
<h1 class="text-2xl font-bold">Real-Time Transcription</h1>
<p class="text-blue-100">Using Articulate.js</p>
</div>
<!-- Main Content -->
<div class="p-6">
<!-- Status Indicator -->
<div id="status" class="mb-4 flex items-center">
<div id="statusDot" class="w-3 h-3 rounded-full bg-gray-400 mr-2"></div>
<span id="statusText" class="text-sm">Ready to start</span>
</div>
<!-- Transcription Output -->
<div class="mb-6">
<label class="block text-gray-700 font-medium mb-2">Transcription</label>
<div id="transcriptOutput" class="min-h-32 border border-gray-300 rounded-lg p-4 bg-gray-50 overflow-y-auto max-h-64">
<p id="interimText" class="text-gray-500 italic">Your transcription will appear here...</p>
<p id="finalText" class="text-gray-800"></p>
</div>
</div>
<!-- Controls -->
<div class="flex flex-wrap gap-3">
<button id="startBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd" />
</svg>
Start
</button>
<button id="stopBtn" disabled class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center opacity-50 cursor-not-allowed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd" />
</svg>
Stop
</button>
<button id="clearBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Clear
</button>
<!-- Language Selector -->
<select id="languageSelect" class="border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="en-US">English (US)</option>
<option value="en-GB">English (UK)</option>
<option value="es-ES">Spanish</option>
<option value="fr-FR">French</option>
<option value="de-DE">German</option>
</select>
</div>
</div>
<!-- Footer -->
<div class="bg-gray-100 p-3 text-center text-sm text-gray-600">
<p>Click "Start" and begin speaking. Transcription will appear in real-time.</p>
</div>
</div>
</div>
</Layout>
<script is:inline src="https://cdn.jsdelivr.net/npm/@articulate/chromeless@1.3.0-beta5/dist/src/index.js"></script>
<script is:inline>
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const clearBtn = document.getElementById('clearBtn');
const languageSelect = document.getElementById('languageSelect');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
const interimText = document.getElementById('interimText');
const finalText = document.getElementById('finalText');
// Initialize Articulate.js
const articulate = new Articulate();
let isListening = false;
// Configure Articulate
function configureArticulate() {
articulate.configure({
continuous: true,
interimResults: true,
language: languageSelect.value,
onError: (error) => {
console.error('Recognition error:', error);
updateStatus('Error: ' + error.message, 'red');
}
});
}
// Update status UI
function updateStatus(text, color = 'gray') {
statusText.textContent = text;
statusDot.className = `w-3 h-3 rounded-full mr-2 bg-${color}-500`;
// Handle special colors
if (color === 'green') {
statusDot.classList.add('animate-pulse');
} else {
statusDot.classList.remove('animate-pulse');
}
}
// Start transcription
startBtn.addEventListener('click', () => {
configureArticulate();
articulate.start()
.on('transcript', (text, isFinal) => {
if (isFinal) {
finalText.textContent += text + ' ';
interimText.textContent = '';
} else {
interimText.textContent = text;
}
// Auto-scroll to bottom
document.getElementById('transcriptOutput').scrollTop =
document.getElementById('transcriptOutput').scrollHeight;
})
.on('start', () => {
isListening = true;
updateStatus('Listening...', 'green');
startBtn.disabled = true;
stopBtn.disabled = false;
stopBtn.classList.remove('opacity-50', 'cursor-not-allowed');
});
});
// Stop transcription
stopBtn.addEventListener('click', () => {
articulate.stop();
isListening = false;
updateStatus('Ready to continue', 'blue');
startBtn.disabled = false;
stopBtn.disabled = true;
stopBtn.classList.add('opacity-50', 'cursor-not-allowed');
});
// Clear transcription
clearBtn.addEventListener('click', () => {
finalText.textContent = '';
interimText.textContent = 'Your transcription will appear here...';
});
// Change language
languageSelect.addEventListener('change', () => {
if (isListening) {
articulate.stop();
configureArticulate();
articulate.start();
}
});
// Initial configuration
configureArticulate();
updateStatus('Ready to start', 'blue');
});
</script>

View File

@ -0,0 +1,73 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="DeepSpeech Streaming STT">
<h1>DeepSpeech Real-Time Transcription</h1>
<button id="startBtn">Start Listening</button>
<button id="stopBtn" disabled>Stop</button>
<div id="transcript"></div>
</Layout>
<script is:inline>
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const transcriptDiv = document.getElementById('transcript');
let mediaStream;
let audioContext;
let processor;
let websocket;
startBtn.addEventListener('click', async () => {
try {
// Initialize WebSocket
websocket = new WebSocket('ws://localhost:3000');
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
transcriptDiv.innerHTML += data.transcript + (data.isFinal ? '<br>' : ' ');
};
// Get microphone
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Set up audio processing (16kHz, mono for DeepSpeech)
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaStreamSource(mediaStream);
// Resample to 16kHz (DeepSpeech requirement)
const downsampler = audioContext.createScriptProcessor(4096, 1, 1);
downsampler.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const pcm16 = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcm16[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
}
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(pcm16.buffer);
}
};
source.connect(downsampler);
downsampler.connect(audioContext.destination);
// Notify server to start processing
websocket.onopen = () => {
websocket.send('start');
startBtn.disabled = true;
stopBtn.disabled = false;
};
} catch (err) {
console.error('Error:', err);
}
});
stopBtn.addEventListener('click', () => {
if (mediaStream) mediaStream.getTracks().forEach(track => track.stop());
if (websocket) websocket.send('end');
startBtn.disabled = false;
stopBtn.disabled = true;
});
</script>

View File

@ -0,0 +1,40 @@
---
import Layout from "../../layouts/Layout.astro";
import ImageResizes from "../../components/Tools/ImageResize"
---
<Layout title="">
<div>
<ImageResizes client:load />
<!-- <input type="file" id="fileInput" accept="image/*" /> -->
</div>
</Layout>
<!-- <script is:inline type="module">
// Import the library directly from CDN (as ES module)
import ImageResizeCompress from 'https://cdn.jsdelivr.net/npm/image-resize-compress/dist/index.min.js';
</script>
<script is:inline>
document.getElementById('fileInput').addEventListener('change', async function(e) {
const file = e.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
console.error('Selected file is not an image');
return;
}
try {
const resizedBlob = await ImageResizeCompress.fromBlob(
file,
75,
0,
0,
'webp'
);
console.log('Resized image:', resizedBlob);
} catch (error) {
console.error('Error resizing image:', error);
}
});
</script> -->

View File

@ -0,0 +1,276 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div class="bg-gradient-to-br from-indigo-50 to-blue-100 min-h-screen">
<div class="container mx-auto px-4 py-12">
<div class="max-w-3xl mx-auto">
<!-- Header -->
<div class="text-center mb-10">
<h1 class="text-4xl font-bold text-indigo-800 mb-2">VoiceCraft Pro</h1>
<p class="text-lg text-indigo-600">Convert text to natural sounding speech</p>
</div>
<!-- Main Card -->
<div class="bg-white rounded-xl shadow-xl overflow-hidden">
<!-- Text Area -->
<div class="p-6 border-b border-gray-200">
<label for="text-to-speak" class="block text-sm font-medium text-gray-700 mb-2">Enter your text</label>
<textarea id="text-to-speak" rows="6" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="Type or paste your text here..."></textarea>
</div>
<!-- Controls -->
<div class="p-6 border-b border-gray-200">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Voice Selection -->
<div>
<label for="voice-select" class="block text-sm font-medium text-gray-700 mb-2">Select Voice</label>
<select id="voice-select" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<option value="">Loading voices...</option>
</select>
</div>
<!-- Pitch Control -->
<div>
<label for="pitch" class="block text-sm font-medium text-gray-700 mb-2">
<svg class="icon inline mr-1" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.73-2.77-.01-2.2-1.9-2.96-3.66-3.42z"/>
</svg>
Pitch: <span id="pitch-value">1</span>
</label>
<input type="range" id="pitch" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
</div>
<!-- Rate Control -->
<div>
<label for="rate" class="block text-sm font-medium text-gray-700 mb-2">
<svg class="icon inline mr-1" viewBox="0 0 24 24">
<path d="M13.5 5.5c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zM9.8 8.9L7 23h2.1l1.8-8 2.1 2v6h2v-7.5l-2.1-2 .6-3C14.8 12 16.8 13 19 13v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1L6 8.3V13h2V9.6l1.8-.7"/>
</svg>
Speed: <span id="rate-value">1</span>
</label>
<input type="range" id="rate" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
</div>
<!-- Volume Control -->
<div>
<label for="volume" class="block text-sm font-medium text-gray-700 mb-2">
<svg class="icon inline mr-1" viewBox="0 0 24 24">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
Volume: <span id="volume-value">1</span>
</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600">
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="p-6 flex flex-wrap justify-between items-center gap-4">
<div class="flex gap-3">
<button id="speak-btn" class="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-medium transition flex items-center gap-2">
&#9654; <!-- Play symbol --> Speak
</button>
<button id="pause-btn" class="px-6 py-3 bg-amber-500 hover:bg-amber-600 text-white rounded-lg font-medium transition flex items-center gap-2">
&#10074;&#10074; <!-- Pause symbol --> Pause
</button>
<button id="resume-btn" class="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition flex items-center gap-2">
&#9654; <!-- Play symbol --> Resume
</button>
<button id="stop-btn" class="px-6 py-3 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition flex items-center gap-2">
&#9632; <!-- Stop symbol --> Stop
</button>
</div>
<button id="download-btn" class="px-6 py-3 bg-gray-800 hover:bg-gray-900 text-white rounded-lg font-medium transition flex items-center gap-2">
<svg class="icon" viewBox="0 0 24 24">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
Download Audio
</button>
</div>
</div>
<!-- Status -->
<div id="status" class="mt-4 text-center text-gray-500 text-sm"></div>
</div>
</div>
</div>
</Layout>
<script is:inline>
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const textInput = document.getElementById('text-to-speak');
const voiceSelect = document.getElementById('voice-select');
const pitchInput = document.getElementById('pitch');
const rateInput = document.getElementById('rate');
const volumeInput = document.getElementById('volume');
const pitchValue = document.getElementById('pitch-value');
const rateValue = document.getElementById('rate-value');
const volumeValue = document.getElementById('volume-value');
const speakBtn = document.getElementById('speak-btn');
const pauseBtn = document.getElementById('pause-btn');
const resumeBtn = document.getElementById('resume-btn');
const stopBtn = document.getElementById('stop-btn');
const downloadBtn = document.getElementById('download-btn');
const statusEl = document.getElementById('status');
// Speech synthesis
const synth = window.speechSynthesis;
let voices = [];
let utterance = null;
let audioChunks = [];
let mediaRecorder;
let audioBlob;
// Update display values
pitchInput.addEventListener('input', () => pitchValue.textContent = pitchInput.value);
rateInput.addEventListener('input', () => rateValue.textContent = rateInput.value);
volumeInput.addEventListener('input', () => volumeValue.textContent = volumeInput.value);
// Load voices
function loadVoices() {
voices = synth.getVoices();
voiceSelect.innerHTML = '';
voices.forEach(voice => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})${voice.default ? ' — DEFAULT' : ''}`;
option.setAttribute('data-name', voice.name);
option.setAttribute('data-lang', voice.lang);
voiceSelect.appendChild(option);
});
if (voices.length === 0) {
statusEl.textContent = 'No voices available. Please try again later.';
} else {
statusEl.textContent = `${voices.length} voices loaded.`;
}
}
// Chrome needs this
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = loadVoices;
}
// Initialize
loadVoices();
// Speak function
function speak() {
if (synth.speaking) {
synth.cancel();
}
if (textInput.value.trim() === '') {
statusEl.textContent = 'Please enter some text first.';
return;
}
utterance = new SpeechSynthesisUtterance(textInput.value);
const selectedOption = voiceSelect.selectedOptions[0];
if (selectedOption) {
const selectedVoice = voices.find(voice =>
voice.name === selectedOption.getAttribute('data-name') &&
voice.lang === selectedOption.getAttribute('data-lang')
);
if (selectedVoice) {
utterance.voice = selectedVoice;
}
}
utterance.pitch = parseFloat(pitchInput.value);
utterance.rate = parseFloat(rateInput.value);
utterance.volume = parseFloat(volumeInput.value);
// Set up MediaRecorder for audio capture
setupAudioCapture();
utterance.onstart = () => {
statusEl.textContent = 'Speaking...';
mediaRecorder.start();
};
utterance.onend = () => {
statusEl.textContent = 'Speech finished.';
mediaRecorder.stop();
};
utterance.onerror = (event) => {
statusEl.textContent = `Error occurred: ${event.error}`;
};
synth.speak(utterance);
}
// Set up audio capture for download
function setupAudioCapture() {
audioChunks = [];
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const destination = audioContext.createMediaStreamDestination();
mediaRecorder = new MediaRecorder(destination.stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
};
}
// Button event listeners
speakBtn.addEventListener('click', speak);
pauseBtn.addEventListener('click', () => {
if (synth.speaking) {
synth.pause();
statusEl.textContent = 'Speech paused.';
}
});
resumeBtn.addEventListener('click', () => {
if (synth.paused) {
synth.resume();
statusEl.textContent = 'Resuming speech...';
}
});
stopBtn.addEventListener('click', () => {
if (synth.speaking || synth.paused) {
synth.cancel();
statusEl.textContent = 'Speech stopped.';
}
});
downloadBtn.addEventListener('click', () => {
if (!audioBlob) {
statusEl.textContent = 'No audio to download. Please speak first.';
return;
}
const url = URL.createObjectURL(audioBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'speech-output.wav';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
statusEl.textContent = 'Audio downloaded successfully.';
});
});
</script>
<style>
.icon {
width: 1em;
height: 1em;
vertical-align: -0.125em;
fill: currentColor;
}
</style>

View File

@ -0,0 +1,70 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div>
<h1>Voice Recognition Demo</h1>
<button id="startBtn">Start Recording</button>
<button id="stopBtn" disabled>Stop Recording</button>
<div id="result"></div>
</div>
</Layout>
<script is:inline>
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const resultDiv = document.getElementById('result');
let mediaRecorder;
let audioChunks = [];
const serverUrl = 'https://transcribe.teachertrainingkolkata.in/transcribe';
startBtn.addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus',
audioBitsPerSecond: 16000
});
mediaRecorder.ondataavailable = (e) => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
audioChunks = [];
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
try {
const response = await fetch(serverUrl, {
method: 'POST',
body: formData
});
const data = await response.json();
resultDiv.textContent = data.text || "No speech detected";
} catch (error) {
console.error('Error:', error);
resultDiv.textContent = 'Error sending audio to server';
}
};
mediaRecorder.start(1000); // Collect data every 1 second
startBtn.disabled = true;
stopBtn.disabled = false;
resultDiv.textContent = "Recording...";
} catch (error) {
console.error('Error:', error);
resultDiv.textContent = 'Error accessing microphone';
}
});
stopBtn.addEventListener('click', () => {
mediaRecorder.stop();
startBtn.disabled = false;
stopBtn.disabled = true;
resultDiv.textContent = "Processing...";
});
</script>

View File

@ -0,0 +1,248 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div class="min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-3xl">
<h1 class="text-3xl font-bold mb-6 text-center">🎙️ Speech to Text</h1>
<div class="bg-gray-200 rounded-lg shadow-md p-6">
<!-- Controls -->
<div class="flex flex-wrap gap-3 mb-6">
<button id="startBtn" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed">
Start Recording
</button>
<button id="stopBtn" disabled class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:cursor-not-allowed hidden">
Stop Recording
</button>
<button id="clearBtn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Clear Text
</button>
</div>
<!-- Status -->
<div id="status" class="px-4 py-3 rounded-md mb-6 bg-gray-100 text-gray-700">
Ready to start
</div>
<!-- Transcription -->
<div id="transcription" class="p-4 border border-gray-200 rounded-md bg-gray-50 min-h-32 max-h-96 overflow-y-auto text-gray-700">
Transcribed text will appear here...
</div>
</div>
</div>
</div>
</Layout>
<script is:inline>
class SpeechToTextApp {
constructor() {
this.ws = null;
this.audioContext = null;
this.processor = null;
this.stream = null;
this.isRecording = false;
this.startBtn = document.getElementById('startBtn');
this.stopBtn = document.getElementById('stopBtn');
this.clearBtn = document.getElementById('clearBtn');
this.status = document.getElementById('status');
this.transcription = document.getElementById('transcription');
this.initializeEventListeners();
this.connectWebSocket();
}
initializeEventListeners() {
this.startBtn.addEventListener('click', () => this.startRecording());
this.stopBtn.addEventListener('click', () => this.stopRecording());
this.clearBtn.addEventListener('click', () => this.clearTranscription());
}
connectWebSocket() {
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// const wsUrl = `${wsProtocol}//${window.location.host}`;
const wsUrl = `${wsProtocol}//${`localhost:3000`}`;
console.log(wsUrl)
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.updateStatus('Connected to server', 'bg-green-100 text-green-700');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'transcription' && data.text) {
this.appendTranscription(data.text);
}
};
this.ws.onclose = () => {
this.updateStatus('Disconnected from server', 'bg-red-100 text-red-700');
setTimeout(() => this.connectWebSocket(), 3000);
};
this.ws.onerror = (error) => {
this.updateStatus('WebSocket error', 'bg-red-100 text-red-700');
};
}
async startRecording() {
try {
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: 16000
});
const source = this.audioContext.createMediaStreamSource(this.stream);
await this.audioContext.audioWorklet.addModule('data:text/javascript,' + encodeURIComponent(`
class AudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.bufferSize = 4096;
this.buffer = new Float32Array(this.bufferSize);
this.bufferIndex = 0;
}
process(inputs) {
const input = inputs[0];
if (input.length > 0) {
const audioData = input[0];
for (let i = 0; i < audioData.length; i++) {
this.buffer[this.bufferIndex] = audioData[i];
this.bufferIndex++;
if (this.bufferIndex >= this.bufferSize) {
// Convert to WAV format
const int16Array = new Int16Array(this.bufferSize);
for (let j = 0; j < this.bufferSize; j++) {
int16Array[j] = Math.max(-32768, Math.min(32767, this.buffer[j] * 32768));
}
// Create WAV header
const wavBuffer = this.createWAVBuffer(int16Array);
this.port.postMessage(wavBuffer);
this.bufferIndex = 0;
}
}
}
return true;
}
createWAVBuffer(samples) {
const length = samples.length;
const buffer = new ArrayBuffer(44 + length * 2);
const view = new DataView(buffer);
// WAV header
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, 36 + length * 2, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, 16000, true);
view.setUint32(28, 16000 * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(36, 'data');
view.setUint32(40, length * 2, true);
// Convert samples to bytes
let offset = 44;
for (let i = 0; i < length; i++) {
view.setInt16(offset, samples[i], true);
offset += 2;
}
return buffer;
}
}
registerProcessor('audio-processor', AudioProcessor);
`));
this.processor = new AudioWorkletNode(this.audioContext, 'audio-processor');
this.processor.port.onmessage = (event) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(event.data);
}
};
source.connect(this.processor);
this.isRecording = true;
this.startBtn.disabled = true;
this.stopBtn.disabled = false;
this.stopBtn.classList.remove('hidden');
this.startBtn.textContent = 'Streaming...';
this.startBtn.classList.remove('bg-green-600', 'hover:bg-green-700');
this.startBtn.classList.add('bg-red-600', 'hover:bg-red-700');
this.updateStatus('🔴 Streaming...', 'bg-green-100 text-green-700');
} catch (error) {
this.updateStatus('Error accessing microphone: ' + error.message, 'bg-red-100 text-red-700');
console.error('Error starting recording:', error);
}
}
stopRecording() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
if (this.audioContext) {
this.audioContext.close();
}
this.isRecording = false;
this.startBtn.disabled = false;
this.stopBtn.disabled = true;
this.stopBtn.classList.add('hidden');
this.startBtn.textContent = 'Start Recording';
this.startBtn.classList.remove('bg-red-600', 'hover:bg-red-700');
this.startBtn.classList.add('bg-green-600', 'hover:bg-green-700');
this.updateStatus('Recording stopped', 'bg-green-100 text-green-700');
}
clearTranscription() {
this.transcription.textContent = 'Transcribed text will appear here...';
this.transcription.classList.add('text-gray-500');
}
appendTranscription(text) {
if (this.transcription.textContent === 'Transcribed text will appear here...') {
this.transcription.textContent = '';
this.transcription.classList.remove('text-gray-500');
}
this.transcription.textContent += text + ' ';
this.transcription.scrollTop = this.transcription.scrollHeight;
}
updateStatus(message, classes = 'bg-gray-100 text-gray-700') {
this.status.textContent = message;
this.status.className = `px-4 py-3 rounded-md mb-6 ${classes}`;
}
}
document.addEventListener('DOMContentLoaded', () => {
new SpeechToTextApp();
});
</script>

View File

@ -0,0 +1,100 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="">
<div>
<button id="startBtn">Start Speaking</button>
<button id="stopBtn">Stop</button>
<div id="outputText" style="min-height: 100px; border: 1px solid #ccc; padding: 10px;"></div>
</div>
</Layout>
<script is:inline>
/**
* Speech-to-Text function that listens to microphone input and displays transcribed text
* @param {HTMLElement} outputElement - Element where the transcribed text will be displayed
* @param {function} onResult - Optional callback function that receives the transcribed text
* @returns {object} An object with start and stop methods to control the recognition
*/
function speechToText(outputElement, onResult) {
// Check if browser supports the Web Speech API
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Your browser does not support speech recognition. Try Chrome or Edge.');
return;
}
// Create speech recognition object
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
// Configure recognition settings
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US'; // Set language - change as needed
let finalTranscript = '';
// Event handler for recognition results
recognition.onresult = (event) => {
let interimTranscript = '';
// Clear final transcript at the start of new session
if (event.resultIndex === 0) {
finalTranscript = '';
}
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
// Replace rather than append to avoid duplicates
finalTranscript = transcript;
if (onResult) onResult(finalTranscript);
} else {
interimTranscript = transcript;
}
}
// Update the output element
if (outputElement) {
outputElement.innerHTML = finalTranscript + (interimTranscript ? '<span style="color:#999">' + interimTranscript + '</span>' : '');
}
};
recognition.onerror = (event) => {
console.error('Speech recognition error', event.error);
};
recognition.onend = () => {
console.log('Speech recognition ended');
};
// Return control methods
return {
start: () => recognition.start(),
stop: () => recognition.stop(),
abort: () => recognition.abort(),
getFinalTranscript: () => finalTranscript
};
}
// Get DOM elements
const outputElement = document.getElementById('outputText');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
// Initialize speech recognition
const recognizer = speechToText(outputElement, (finalText) => {
console.log('Final text:', finalText);
});
// Add button event listeners
startBtn.addEventListener('click', () => {
outputElement.textContent = 'Listening...';
recognizer.start();
});
stopBtn.addEventListener('click', () => {
recognizer.stop();
console.log('Final transcript:', recognizer.getFinalTranscript());
});
</script>

View File

@ -0,0 +1,276 @@
---
import Layout from "../../layouts/Layout.astro"
---
<Layout title="">
<div class="min-h-screen">
<div class="container mx-auto px-4 py-8">
<div class="max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Whisper.cpp STT Streaming</h1>
<div class="mb-6">
<div class="flex justify-center space-x-4 mb-4">
<button id="startBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
Start Recording
</button>
<button id="stopBtn" disabled class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
Stop Recording
</button>
<button id="clearBtn" class="bg-gray-500 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded">
Clear Text
</button>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="language">
Language
</label>
<select id="language" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
<option value="auto">Auto-detect</option>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="zh">Chinese</option>
</select>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="model">
Model
</label>
<select id="model" class="shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
<option value="tiny">Tiny</option>
<option value="base">Base</option>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large" selected>Large</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="status">
Status
</label>
<div id="status" class="bg-gray-100 p-3 rounded text-sm text-gray-700">
Ready to start recording...
</div>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="transcript">
Transcript
</label>
<div id="transcript" class="bg-gray-50 p-4 rounded min-h-32 border border-gray-200">
<!-- Transcript will appear here -->
</div>
</div>
<div class="text-xs text-gray-500 mt-6">
<p>Note: This interface connects to a whisper.cpp server for processing. Audio is streamed in real-time.</p>
</div>
</div>
</div>
</div>
</Layout>
<script is:inline>
// DOM Elements
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const clearBtn = document.getElementById('clearBtn');
const statusDiv = document.getElementById('status');
const transcriptDiv = document.getElementById('transcript');
const languageSelect = document.getElementById('language');
const modelSelect = document.getElementById('model');
// Audio context and variables
let audioContext;
let mediaStream;
let processor;
let audioSocket;
let silenceTimeout;
const SILENCE_THRESHOLD = 0.02; // Adjust based on testing
const SILENCE_TIMEOUT_MS = 2000; // 2 seconds of silence before stopping
// WebSocket URL - adjust to your whisper.cpp server
const WS_URL = 'ws://localhost:8765';
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Check for WebAudio API support
if (!window.AudioContext && !window.webkitAudioContext) {
statusDiv.textContent = 'Web Audio API not supported in this browser';
startBtn.disabled = true;
return;
}
// Check for WebSocket support
if (!window.WebSocket) {
statusDiv.textContent = 'WebSocket not supported in this browser';
startBtn.disabled = true;
return;
}
});
// Event Listeners
startBtn.addEventListener('click', startRecording);
stopBtn.addEventListener('click', stopRecording);
clearBtn.addEventListener('click', clearTranscript);
async function startRecording() {
try {
statusDiv.textContent = 'Requesting microphone...';
// Get microphone access
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Initialize audio context
audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaStreamSource(mediaStream);
// Create script processor for audio processing
processor = audioContext.createScriptProcessor(4096, 1, 1);
// Connect audio nodes
source.connect(processor);
processor.connect(audioContext.destination);
// Initialize WebSocket connection
audioSocket = new WebSocket(WS_URL);
audioSocket.onopen = () => {
statusDiv.textContent = 'Connected to server. Recording...';
startBtn.disabled = true;
stopBtn.disabled = false;
// Send configuration
audioSocket.send(JSON.stringify({
type: 'config',
language: languageSelect.value,
model: modelSelect.value
}));
};
audioSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'transcript') {
// Append to transcript
const p = document.createElement('p');
p.className = 'mb-2';
p.textContent = data.text;
transcriptDiv.appendChild(p);
// Scroll to bottom
transcriptDiv.scrollTop = transcriptDiv.scrollHeight;
} else if (data.type === 'status') {
statusDiv.textContent = data.message;
} else if (data.type === 'error') {
statusDiv.textContent = `Error: ${data.message}`;
stopRecording();
}
};
audioSocket.onclose = () => {
if (statusDiv.textContent !== 'Recording stopped.') {
statusDiv.textContent = 'Connection closed unexpectedly.';
}
cleanup();
};
audioSocket.onerror = (error) => {
statusDiv.textContent = `WebSocket error: ${error.message}`;
cleanup();
};
// Process audio data
processor.onaudioprocess = (event) => {
if (!audioSocket || audioSocket.readyState !== WebSocket.OPEN) return;
const audioData = event.inputBuffer.getChannelData(0);
// Check for silence
const isSilent = isAudioSilent(audioData);
if (!isSilent) {
// Reset silence timeout
clearTimeout(silenceTimeout);
silenceTimeout = setTimeout(() => {
statusDiv.textContent = 'Silence detected, stopping recording...';
stopRecording();
}, SILENCE_TIMEOUT_MS);
// Convert Float32Array to Int16Array for WebSocket
const int16Data = convertFloat32ToInt16(audioData);
// Send audio data
audioSocket.send(int16Data);
}
};
} catch (error) {
statusDiv.textContent = `Error: ${error.message}`;
console.error(error);
cleanup();
}
}
function stopRecording() {
statusDiv.textContent = 'Recording stopped.';
if (audioSocket && audioSocket.readyState === WebSocket.OPEN) {
audioSocket.send(JSON.stringify({ type: 'eof' }));
audioSocket.close();
}
cleanup();
}
function clearTranscript() {
transcriptDiv.innerHTML = '';
}
function cleanup() {
if (processor) {
processor.disconnect();
processor = null;
}
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
if (audioContext) {
audioContext.close().catch(console.error);
audioContext = null;
}
clearTimeout(silenceTimeout);
startBtn.disabled = false;
stopBtn.disabled = true;
}
// Helper functions
function isAudioSilent(audioData) {
// Calculate RMS (root mean square) of the audio buffer
let sum = 0;
for (let i = 0; i < audioData.length; i++) {
sum += audioData[i] * audioData[i];
}
const rms = Math.sqrt(sum / audioData.length);
return rms < SILENCE_THRESHOLD;
}
function convertFloat32ToInt16(buffer) {
const length = buffer.length;
const int16Array = new Int16Array(length);
for (let i = 0; i < length; i++) {
const s = Math.max(-1, Math.min(1, buffer[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return int16Array.buffer;
}
</script>

View File

@ -3136,6 +3136,11 @@ ieee754@^1.2.1:
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
image-resize-compress@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/image-resize-compress/-/image-resize-compress-2.1.1.tgz"
integrity sha512-aF4O1ZzAM0wDziIQZdqlxvDBe163J1Kiu+hSXbYNaGDcD4ggqAgRLLtRmpspnpJPHa4PkhVjwOshzBMHbOWESQ==
import-fresh@^3.3.0:
version "3.3.1"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"