Merge pull request 'add image labeling and previous many works' (#39) from seow into main
Reviewed-on: #39main
commit
e23db74eb8
|
@ -5,18 +5,31 @@ import react from '@astrojs/react';
|
|||
|
||||
export default defineConfig({
|
||||
site: 'https://siliconpin.cs1.hz.siliconpin.com',
|
||||
integrations: [tailwind(), react()],
|
||||
|
||||
// Vite-specific settings (including proxy)
|
||||
vite: {
|
||||
server: {
|
||||
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080', // Your backend server
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
secure: false, // Only needed if using self-signed HTTPS
|
||||
}
|
||||
}
|
||||
},
|
||||
preview: {
|
||||
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
|
||||
}
|
||||
},
|
||||
integrations: [tailwind(), react()],
|
||||
|
||||
// Astro server settings
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
host: '0.0.0.0', // Accessible on all network interfaces
|
||||
port: 4000,
|
||||
},
|
||||
|
||||
output: 'static',
|
||||
});
|
|
@ -25,7 +25,9 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
"image-resize-compress": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.8",
|
||||
"menubar": "^9.5.1",
|
||||
|
@ -6747,6 +6749,33 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.18.1",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.18.1.tgz",
|
||||
"integrity": "sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.18.1",
|
||||
"motion-utils": "^12.18.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
||||
|
@ -8192,6 +8221,15 @@
|
|||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -12910,6 +12948,21 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.18.1",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.18.1.tgz",
|
||||
"integrity": "sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.18.1",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.18.1.tgz",
|
||||
"integrity": "sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"build": "astro build",
|
||||
"preview": "astro preview --port 4000",
|
||||
"astro": "astro",
|
||||
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ u2@siliconpin.com:~/web/siliconpin.com/public_html/"
|
||||
"push-prod": "rsync -rv --exclude .hta_config/conf.php dist/ u2@siliconpin.com:~/web/siliconpin.com/public_html/",
|
||||
"proxy": "http://localhost:8080"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^4.2.1",
|
||||
|
@ -27,7 +28,9 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
"image-resize-compress": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.8",
|
||||
"menubar": "^9.5.1",
|
||||
|
|
|
@ -6,7 +6,7 @@ import { X } from "lucide-react";
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('https://tst-pb.s38.siliconpin.com');
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
export function AvatarUpload({ userId }: { userId: string }) {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function UserBillingList() {
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(20);
|
||||
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Loader2 } from "lucide-react";
|
|||
|
||||
export default function BuyVPN() {
|
||||
// API URL - make sure to set this correctly
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
// State for VPN configuration
|
||||
const [vpnContinent, setVpnContinent] = useState("");
|
||||
|
|
|
@ -10,7 +10,7 @@ const PRICE_CONFIG = [
|
|||
{purchaseType: 'bulk', price: 1000, minute: 10000}
|
||||
];
|
||||
export default function STTStreaming(){
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
const [purchaseType, setPurchaseType] = useState();
|
||||
const [amount, setAmount] = useState();
|
||||
const [dataError, setDataError] = useState();
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
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 { Textarea } from "./ui/textarea";
|
||||
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';
|
||||
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||
|
||||
export default function Comment(props) {
|
||||
const [comments, setComments] = useState([]);
|
||||
|
@ -17,38 +14,9 @@ export default function Comment(props) {
|
|||
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
|
||||
// Load comments when component mounts or when topicId changes
|
||||
useEffect(() => {
|
||||
if (props.topicId) {
|
||||
fetchComments(props.topicId);
|
||||
|
@ -81,84 +49,12 @@ export default function Comment(props) {
|
|||
}
|
||||
};
|
||||
|
||||
// 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 = ``;
|
||||
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 = ``;
|
||||
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 handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setNewComment(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmitComment = async (e) => {
|
||||
|
@ -214,7 +110,7 @@ export default function Comment(props) {
|
|||
|
||||
{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 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>
|
||||
|
@ -240,9 +136,7 @@ export default function Comment(props) {
|
|||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div data-color-mode="light" className="markdown-body">
|
||||
<MDEditor.Markdown source={comment.comment} />
|
||||
</div>
|
||||
<p className="">{comment.comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -250,7 +144,7 @@ export default function Comment(props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Comments Section with MDEditor */}
|
||||
{/* Comments Section */}
|
||||
<Card className="mt-16 border-t">
|
||||
<div className="relative">
|
||||
<div className="px-6 pb-6 rounded-lg">
|
||||
|
@ -268,87 +162,22 @@ export default function Comment(props) {
|
|||
<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>
|
||||
<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>
|
||||
|
||||
{/* 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" />
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import CommentThread from './CommentThread';
|
||||
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||
const CommentSystem = ({ topicId, pbId }) => {
|
||||
const isLoginCoockie = JSON.parse(isLogin);
|
||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||
const [comments, setComments] = useState([]);
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [commentsLoading, setCommentsLoading] = useState(false);
|
||||
const [commentsError, setCommentsError] = useState('');
|
||||
|
||||
const nestComments = (flatComments) => {
|
||||
const commentMap = {};
|
||||
const rootComments = [];
|
||||
|
||||
flatComments?.forEach(comment => {
|
||||
comment.replies = [];
|
||||
commentMap[comment.comment_id] = comment;
|
||||
});
|
||||
|
||||
flatComments?.forEach(comment => {
|
||||
if (comment.parent_comment_id?.Valid) {
|
||||
const parent = commentMap[comment.parent_comment_id.String];
|
||||
if (parent) parent.replies.push(comment);
|
||||
} else {
|
||||
rootComments.push(comment);
|
||||
}
|
||||
});
|
||||
|
||||
return rootComments.sort((a, b) =>
|
||||
new Date(b.created_at) - new Date(a.created_at));
|
||||
};
|
||||
|
||||
const fetchComments = async () => {
|
||||
try {
|
||||
setCommentsLoading(true);
|
||||
setCommentsError('');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
topic_id: String(topicId),
|
||||
});
|
||||
|
||||
// Only add silicon_id if it has a value
|
||||
if (siliconId && siliconId !== 'null') {
|
||||
params.append('silicon_id', siliconId);
|
||||
}
|
||||
|
||||
// Only add pb_id if it has a value
|
||||
if (pb_id && pb_id !== 'null') {
|
||||
params.append('pb_id', pb_id);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/comments?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const flatComments = await response.json();
|
||||
setComments(nestComments(flatComments || []));
|
||||
} catch (err) {
|
||||
setCommentsError(err.message);
|
||||
console.error('Fetch error:', err);
|
||||
} finally {
|
||||
setCommentsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const postComment = async (text, parentId = null) => {
|
||||
try {
|
||||
setCommentsLoading(true);
|
||||
|
||||
const payload = {
|
||||
topic_id: String(topicId),
|
||||
silicon_id: siliconId,
|
||||
pb_id: pb_id,
|
||||
user_id: siliconId,
|
||||
user_name: user_name,
|
||||
comment_text: text,
|
||||
is_approved: true
|
||||
};
|
||||
|
||||
if (parentId) {
|
||||
payload.parent_comment_id = parentId;
|
||||
}
|
||||
|
||||
const endpoint = parentId ? '/api/comments/reply' : '/api/comments';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || 'Failed to post comment');
|
||||
}
|
||||
|
||||
await fetchComments();
|
||||
} catch (err) {
|
||||
setCommentsError(err.message);
|
||||
} finally {
|
||||
setCommentsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchComments();
|
||||
}, [topicId, siliconId, pbId]);
|
||||
|
||||
return (
|
||||
<div className=" mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-2xl font-bold text-[#6d9e37] mb-6">Comments</h2>
|
||||
|
||||
{commentsError && (
|
||||
<div className="bg-red-50 text-red-700 p-3 rounded-md mb-4">
|
||||
{commentsError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{commentsLoading && comments.length === 0 ? (
|
||||
<div className="text-center py-8 text-[#6d9e37]">Loading comments...</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{comments.length > 0 ? (
|
||||
comments.map(comment => (
|
||||
<CommentThread key={comment.comment_id} comment={comment} onReply={postComment} />
|
||||
))
|
||||
) : (
|
||||
<p className="text-center py-8 text-gray-500 italic">
|
||||
No comments yet. Be the first to comment!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='relative'>
|
||||
{
|
||||
isLoginCoockie !== true ? (
|
||||
<div className="absolute bg-black bg-opacity-70 w-full h-full rounded-lg">
|
||||
<div className='absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2'>
|
||||
<p className='italic bg-[#2a2e35] px-2 py-1.5 rounded-md shadow-xl'>To join the conversation, please log in first.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
<Card className="mt-8 p-6">
|
||||
<h3 className="text-lg font-medium mb-4">Add a Comment</h3>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!newComment.trim()) return;
|
||||
postComment(newComment);
|
||||
setNewComment('');
|
||||
}}>
|
||||
<Textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Share your thoughts..." className="min-h-[100px]" required minLength={1}/>
|
||||
<div className="mt-4">
|
||||
<Button type="submit" disabled={commentsLoading || !newComment.trim()} className="w-full sm:w-auto">
|
||||
{commentsLoading ? (
|
||||
<span className="flex items-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Posting...
|
||||
</span>
|
||||
) : 'Post Comment'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentSystem;
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||
const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||
const isLoginCoockie = JSON.parse(isLogin);
|
||||
const [isReplying, setIsReplying] = useState(false);
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [replyError, setReplyError] = useState('')
|
||||
|
||||
const handleReplySubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!replyText.trim()) return;
|
||||
onReply(replyText, comment.comment_id);
|
||||
setReplyText('');
|
||||
setIsReplying(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mb-4 pl-${depth * 4} border-l-2 ${depth > 0 ? 'border-[#6d9e37]' : 'border-transparent'}`}
|
||||
style={{ marginLeft: `${depth * 0.5}rem` }}
|
||||
>
|
||||
<div className="bg-[#25272950] rounded-lg shadow-sm px-4 py-3 mb-2">
|
||||
<div className="flex gap-3 items-center justify-center">
|
||||
{/* User Avatar SVG */}
|
||||
<div className="flex-shrink-0 ">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" className="rounded-full bg-[#6d9e37] p-1"><path d="M20 20C24.1421 20 27.5 16.6421 27.5 12.5C27.5 8.35786 24.1421 5 20 5C15.8579 5 12.5 8.35786 12.5 12.5C12.5 16.6421 15.8579 20 20 20ZM20 23.75C14.2675 23.75 5 26.4825 5 32.1875V35H35V32.1875C35 26.4825 25.7325 23.75 20 23.75Z" fill="white"/></svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<span className="font-medium text-[#6d9e37]">{comment.user_name}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{new Date(comment.created_at).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-100 mb-2 whitespace-pre-wrap">{comment.comment_text}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isLoginCoockie === true && (
|
||||
<Button variant="outline" className="border-none text-[#6d9e37] hover:text-[#8bc34a] hover:bg-transparent px-0 py-0 h-auto" onClick={() => setIsReplying(!isReplying)}>Reply</Button>
|
||||
)
|
||||
}
|
||||
{isReplying && (
|
||||
<form onSubmit={handleReplySubmit} className="mt-3 pl-11">
|
||||
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write your reply..." className="mb-2 min-h-[70px]" required minLength={1} />
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" size="sm" className="bg-[#6d9e37] hover:bg-[#8bc34a]">Post Reply</Button>
|
||||
<Button type="button" variant="outline" size="sm" className="border-gray-600 text-gray-300 hover:border-[#6d9e37] hover:text-[#8bc34a]" onClick={() => setIsReplying(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{comment.replies?.map((reply) => (
|
||||
<CommentThread key={reply.comment_id} comment={reply} depth={depth + 1} onReply={onReply}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentThread;
|
|
@ -0,0 +1,5 @@
|
|||
import CommentSystem from './CommentSystem';
|
||||
import CommentThread from './CommentThread';
|
||||
|
||||
export default CommentSystem;
|
||||
export { CommentThread };
|
|
@ -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-sxashuasysagibx.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 = ``;
|
||||
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 = ``;
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,7 @@ import { Button } from './ui/button';
|
|||
export function ContactForm() {
|
||||
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 USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
|
|
@ -5,8 +5,8 @@ import { Button } from './ui/button';
|
|||
|
||||
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||
// API URLs
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
||||
const API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
const SERVICES_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/services/';
|
||||
|
||||
// State for deployment options
|
||||
const [deploymentType, setDeploymentType] = useState('app');
|
||||
|
|
|
@ -38,9 +38,9 @@ export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|||
|
||||
|
||||
const [vpnContinent, setVpnContinent] = useState('');
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
||||
// const BILLING_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
const API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
const SERVICES_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/services/';
|
||||
// const BILLING_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/index.php';
|
||||
|
||||
const [selectedcycle, setSelectedcycle] = useState('');
|
||||
const [selectedPrice, setSelectedPrice] = useState(0);
|
||||
|
|
|
@ -48,6 +48,14 @@ 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 handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
@ -123,13 +131,6 @@ 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`, {
|
||||
|
@ -144,22 +145,27 @@ const LoginPage = () => {
|
|||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (response.status === 200 && data.message) {
|
||||
console.log('User exists:', data.message);
|
||||
return data.user; // return the existing user
|
||||
}
|
||||
console.log('New user created:', data);
|
||||
return data; // return the new user
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('MongoDB Response:', data);
|
||||
return data;
|
||||
throw new Error(data.error || `HTTP error! status: ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error('MongoDB Creation Error:', error);
|
||||
throw error; // Return Error
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
// Step 1: Sync with SiliconPin backend
|
||||
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
|
||||
const response = await fetch('https://host-api-sxashuasysagibx.siliconpin.com/v1/users/?query=login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
@ -191,7 +197,14 @@ const LoginPage = () => {
|
|||
|
||||
// Step 3: Redirect to profile if all are ok
|
||||
window.location.href = '/profile';
|
||||
console.log('data', data)
|
||||
setCookie('token', data.userData.accessToken, 7);
|
||||
setCookie('user_name', authData.record.name, 7);
|
||||
setCookie('pb_id', authData.record.id, 7);
|
||||
setCookie('siliconId', data.userData.userId, 7);
|
||||
setCookie('isLogin', JSON.stringify(true), 7);
|
||||
|
||||
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('Sync Error:', error);
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Toast } from './Toast';
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "./ui/card";
|
||||
import { FileX, Copy, CheckCircle2, AlertCircle, X } from "lucide-react";
|
||||
import Loader from "./ui/loader";
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
export default function MakePayment(){
|
||||
const [initialOrderData, setInitialOrderData] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ChevronUp, ChevronDown, Eye, Pencil, Trash2, Download, CircleArrowRight
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
|
||||
export default function AllCustomerList() {
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||
const [usersData, setUsersData] = useState([]);
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
|
|
|
@ -37,7 +37,7 @@ export default function AllSellingList() {
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage] = useState(10);
|
||||
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && sessionData?.user_type === 'admin') {
|
||||
|
@ -774,7 +774,7 @@ export default function AllSellingList() {
|
|||
</div>
|
||||
<div className="">
|
||||
<label htmlFor="remarks" className="block text-sm font-medium text-gray-700 mb-1">Remarks</label>
|
||||
<input type="text" name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950" />
|
||||
<textarea name="remarks" value={formData.remarks} onChange={handleInputChange} className="w-full px-3 py-2 border border-[#6d9e37] rounded-md outline-none text-neutral-950"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export default function ViewAsUser() {
|
|||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
||||
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function TopicCreation(props) {
|
|||
useEffect(() => {
|
||||
const fetchTopics = async () => {
|
||||
try {
|
||||
const res = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=my-topics', {
|
||||
const res = await fetch('https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/?query=my-topics', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
|
|
|
@ -8,7 +8,7 @@ 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 TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
|
||||
|
||||
const NewTopic = () => {
|
||||
|
|
|
@ -7,7 +7,7 @@ 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 TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
|
||||
|
||||
const NewTopic = () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function HestiaCredentialsFetcher() {
|
|||
const [serviceOrderId, setServiceOrderId] = useState(null);
|
||||
const [userSiliconId, setUserSiliconId] = useState();
|
||||
|
||||
const SERVICES_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/';
|
||||
const SERVICES_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/services/';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
|
|
|
@ -135,7 +135,7 @@ const SignupPage = () => {
|
|||
|
||||
const syncSessionWithBackend = async (authData: AuthResponse, avatarUrl: string) => {
|
||||
try {
|
||||
const response = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/users/?query=login', {
|
||||
const response = await fetch('https://host-api-sxashuasysagibx.siliconpin.com/v1/users/?query=login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
|
@ -27,7 +27,7 @@ interface Message {
|
|||
user_type: string;
|
||||
}
|
||||
|
||||
const API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/ticket/index.php';
|
||||
const API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/ticket/index.php';
|
||||
|
||||
function Ticketing() {
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
|
|
|
@ -0,0 +1,304 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
const API_OPTIONS = [
|
||||
{
|
||||
id: 'whisper',
|
||||
name: 'Whisper CPP',
|
||||
endpoint: 'https://stt-41.siliconpin.com/stt',
|
||||
description: 'Fast lightweight speech recognition'
|
||||
},
|
||||
{
|
||||
id: 'vosk',
|
||||
name: 'Vosk CPP',
|
||||
endpoint: 'https://api.vosk.ai/stt',
|
||||
description: 'Offline speech recognition'
|
||||
},
|
||||
];
|
||||
|
||||
const MAX_FILE_SIZE_MB = 5; // 1MB limit
|
||||
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
export default function AudioUploader() {
|
||||
const [file, setFile] = useState(null);
|
||||
const [status, setStatus] = useState('No file uploaded yet');
|
||||
const [response, setResponse] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedApi, setSelectedApi] = useState('whisper');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const selectedFile = e.target.files[0];
|
||||
if (!selectedFile) {
|
||||
setFile(null);
|
||||
setStatus('No file selected');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (selectedFile.size > MAX_FILE_SIZE_BYTES) {
|
||||
setError(`File size exceeds ${MAX_FILE_SIZE_MB}MB limit`);
|
||||
setStatus('File too large');
|
||||
setFile(null);
|
||||
// Clear the file input
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setStatus(`File selected: ${selectedFile.name}`);
|
||||
setResponse(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!file) return;
|
||||
|
||||
// Double check file size before submitting
|
||||
if (file.size > MAX_FILE_SIZE_BYTES) {
|
||||
setError(`File size exceeds ${MAX_FILE_SIZE_MB}MB limit`);
|
||||
setStatus('File too large');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setStatus(`Processing with ${API_OPTIONS.find(api => api.id === selectedApi)?.name}...`);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', file);
|
||||
|
||||
const apiConfig = API_OPTIONS.find(api => api.id === selectedApi);
|
||||
if (!apiConfig) throw new Error('Selected API not found');
|
||||
|
||||
const apiResponse = await fetch(apiConfig.endpoint, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
let errorMessage = `API returned error status: ${apiResponse.status}`;
|
||||
try {
|
||||
const errorData = await apiResponse.json();
|
||||
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||
} catch (e) {}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const result = await apiResponse.json();
|
||||
setResponse({
|
||||
api: selectedApi,
|
||||
data: result
|
||||
});
|
||||
setStatus('Processing complete');
|
||||
} catch (err) {
|
||||
setError(err.message.includes('Failed to fetch')
|
||||
? 'Network error: Could not connect to the API server'
|
||||
: err.message);
|
||||
setStatus('Processing failed');
|
||||
setResponse(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}).catch(err => {
|
||||
console.error('Copy failed:', err);
|
||||
setError('Failed to copy text to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (!response?.data) return null;
|
||||
|
||||
// Handle various API response formats
|
||||
if (typeof response.data === 'string') {
|
||||
return response.data;
|
||||
}
|
||||
if (response.data.text) {
|
||||
return response.data.text;
|
||||
}
|
||||
if (response.data.transcript) {
|
||||
return response.data.transcript;
|
||||
}
|
||||
if (response.data.results?.[0]?.alternatives?.[0]?.transcript) {
|
||||
return response.data.results[0].alternatives[0].transcript;
|
||||
}
|
||||
|
||||
return "Received response but couldn't extract text. View full response for details.";
|
||||
};
|
||||
|
||||
const displayText = getDisplayText();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 max-w-4xl my-6">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h1 className="text-2xl font-bold mb-6 text-gray-800">Audio File to Text Converter</h1>
|
||||
|
||||
{/* File Upload Section */}
|
||||
<div className="mb-8 p-4 border border-dashed border-[#6d9e37] rounded-lg bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Upload Audio File
|
||||
</label>
|
||||
<p className="text-xs text-gray-500">Supports WAV, MP3, OGG formats (max {MAX_FILE_SIZE_MB}MB)</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
variant="outline"
|
||||
className="px-4 py-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
accept="audio/*,.wav,.mp3,.ogg"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{file && (
|
||||
<div className="flex items-center justify-between bg-white p-3 rounded border">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 text-[#6d9e37] mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-[#6d9e37]">{file.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-[#6d9e37]">
|
||||
{(file.size / (1024 * 1024)).toFixed(2)} MB
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* API Selection Section */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-700">Select Speech Recognition API</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{API_OPTIONS.map(api => (
|
||||
<div
|
||||
key={api.id}
|
||||
onClick={() => !isLoading && setSelectedApi(api.id)}
|
||||
className={`p-4 border-[1.5px] rounded-lg cursor-pointer transition-all ${
|
||||
selectedApi === api.id
|
||||
? 'border-[1.5px] border-[#6d9e37] bg-[#6d9e3720]'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
} ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
checked={selectedApi === api.id}
|
||||
onChange={() => {}}
|
||||
className="mr-2 h-4 w-4 accent-[#6d9e37]"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-bold text-[#6d9e37]">{api.name}</h3>
|
||||
<p className="text-xs text-gray-500">{api.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!file || isLoading || (file && file.size > MAX_FILE_SIZE_BYTES)}
|
||||
className="px-6 py-3 text-lg w-full md:w-auto"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Processing...
|
||||
</span>
|
||||
) : (
|
||||
`Convert with ${API_OPTIONS.find(api => api.id === selectedApi)?.name}`
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="mb-6 text-center">
|
||||
<p className={`text-sm ${
|
||||
error ? 'text-red-600' :
|
||||
isLoading ? 'text-[#6d9e37]' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center text-red-600">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h3 className="font-medium">Error</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-red-600">{error}</p>
|
||||
{error.includes(`${MAX_FILE_SIZE_MB}MB`) && (
|
||||
<p className="mt-1 text-xs text-red-500">
|
||||
Please select a smaller audio file
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results Display */}
|
||||
{response && (
|
||||
<div className="mt-6 border rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 px-4 py-3 border-b flex justify-between items-center">
|
||||
<h3 className="font-medium text-[#6d9e37]">{API_OPTIONS.find(api => api.id === response.api)?.name} Results</h3>
|
||||
{displayText && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => copyToClipboard(displayText)}
|
||||
className="text-sm">
|
||||
{copied ? (<span className="flex items-center">Copied!</span>) : 'Copy Text'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white">
|
||||
{displayText ? (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-700">Transcription:</h4>
|
||||
<div className="relative">
|
||||
<p className="p-3 bg-gray-50 rounded text-sm text-gray-600 font-medium">{displayText}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600">
|
||||
No text transcription found in the response.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
function App() {
|
||||
const [images, setImages] = useState(Array(9).fill({ url: null, secretKey: null }));
|
||||
const [loading, setLoading] = useState(Array(9).fill(true));
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
|
||||
// Fetch initial images with staggered loading
|
||||
useEffect(() => {
|
||||
const fetchInitialImages = async () => {
|
||||
try {
|
||||
const newImages = [];
|
||||
const newLoading = [];
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
setTimeout(async () => {
|
||||
const imageData = await fetchImage();
|
||||
setImages(prev => {
|
||||
const updated = [...prev];
|
||||
updated[i] = imageData;
|
||||
return updated;
|
||||
});
|
||||
setLoading(prev => {
|
||||
const updated = [...prev];
|
||||
updated[i] = false;
|
||||
return updated;
|
||||
});
|
||||
}, i * 150); // Staggered loading effect
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setLoading(Array(9).fill(false));
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialImages();
|
||||
}, []);
|
||||
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
const response = await fetch('https://api-img-lbeling-jugtfrvbghysvfr.siliconpin.com/get-image');
|
||||
if (!response.ok) throw new Error('Failed to fetch image');
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching image:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const submitScore = async (imageData, score, index) => {
|
||||
try {
|
||||
setLoading(prev => {
|
||||
const newLoading = [...prev];
|
||||
newLoading[index] = true;
|
||||
return newLoading;
|
||||
});
|
||||
|
||||
const fileName = imageData.url.split('/').pop();
|
||||
const response = await fetch('https://api-img-lbeling-jugtfrvbghysvfr.siliconpin.com/save-score', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fileName: fileName,
|
||||
score: score,
|
||||
secretKey: imageData.secretKey
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to submit score');
|
||||
|
||||
// Animate out the current image
|
||||
setImages(prev => {
|
||||
const newImages = [...prev];
|
||||
newImages[index] = { ...newImages[index], exiting: true };
|
||||
return newImages;
|
||||
});
|
||||
|
||||
// Wait for animation to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Get new image and animate it in
|
||||
const newImage = await fetchImage();
|
||||
setImages(prev => {
|
||||
const newImages = [...prev];
|
||||
newImages[index] = { ...newImage, entering: true };
|
||||
return newImages;
|
||||
});
|
||||
|
||||
// Remove animation flags after entrance
|
||||
setTimeout(() => {
|
||||
setImages(prev => {
|
||||
const newImages = [...prev];
|
||||
newImages[index] = { ...newImages[index], entering: false };
|
||||
return newImages;
|
||||
});
|
||||
}, 300);
|
||||
} catch (err) {
|
||||
console.error('Error submitting score:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(prev => {
|
||||
const newLoading = [...prev];
|
||||
newLoading[index] = false;
|
||||
return newLoading;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderStarButtons = (imageData, index) => {
|
||||
if (!imageData.url) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex justify-center gap-2 mt-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map(star => (
|
||||
<motion.button
|
||||
key={star}
|
||||
onClick={() => submitScore(imageData, star, index)}
|
||||
disabled={loading[index]}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className={`
|
||||
px-4 py-2 rounded-full border-2 font-medium text-sm
|
||||
transition-colors duration-200
|
||||
${loading[index] ?
|
||||
'bg-gray-100 border-gray-200 text-gray-400 cursor-not-allowed' :
|
||||
`bg-white ${star >= 3 ?
|
||||
'border-emerald-300 text-emerald-600 hover:bg-emerald-50' :
|
||||
'border-amber-300 text-amber-600 hover:bg-amber-50'}`
|
||||
}
|
||||
`}
|
||||
>
|
||||
{star} ★
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<motion.div
|
||||
className="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-50 to-purple-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
className="max-w-md p-8 bg-white rounded-2xl shadow-xl text-center"
|
||||
initial={{ scale: 0.9 }}
|
||||
animate={{ scale: 1 }}
|
||||
>
|
||||
<div className="text-red-500 text-2xl font-bold mb-4">Oops!</div>
|
||||
<div className="text-gray-600 mb-6">{error}</div>
|
||||
<motion.button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:shadow-md transition-all"
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
>
|
||||
Try Again
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen py-12 px-4 sm:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold mb-3 text-[#6d9e37]">Image Rating App</h1>
|
||||
<p className="text-lg max-w-2xl mx-auto">Rate these beautiful images from 1 to 5 stars. Your feedback helps improve our collection!</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{images.map((imageData, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{
|
||||
opacity: loading[index] && !imageData.url ? 0.7 : 1,
|
||||
y: 0
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
delay: index * 0.05
|
||||
}}
|
||||
className={`relative bg-white rounded-2xl shadow-lg overflow-hidden
|
||||
transition-all hover:shadow-xl ${loading[index] ? 'animate-pulse' : ''}`}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{imageData.url ? (
|
||||
<motion.div
|
||||
key={`image-${index}`}
|
||||
initial={{ opacity: imageData.exiting ? 1 : 0, scale: imageData.exiting ? 1 : 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="h-72 flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-4 relative">
|
||||
<motion.img
|
||||
src={imageData.url}
|
||||
alt={`Image ${index}`}
|
||||
className="max-h-full max-w-full object-contain cursor-pointer"
|
||||
onClick={() => setSelectedImage(imageData.url)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
onError={(e) => {
|
||||
e.target.onerror = null;
|
||||
e.target.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QxZDFkMSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0zIDNoMTh2MThIM3oiLz48cGF0aCBkPSJNMTkgOUw5IDE5Ii8+PHBhdGggZD0iTTkgOWwxMCAxMCIvPjwvc3ZnPg==';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
{renderStarButtons(imageData, index)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={`skeleton-${index}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.7 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="h-72 bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center"
|
||||
>
|
||||
<div className="text-gray-400">Loading image...</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{loading[index] && imageData.url && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ repeat: Infinity, duration: 1, ease: "linear" }}
|
||||
className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Modal */}
|
||||
<AnimatePresence>
|
||||
{selectedImage && (
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setSelectedImage(null)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0.9 }}
|
||||
className="relative max-w-4xl w-full"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="absolute -top-10 right-0 text-white text-3xl hover:text-gray-300 transition"
|
||||
onClick={() => setSelectedImage(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt="Enlarged view"
|
||||
className="max-h-[80vh] w-full object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -5,9 +5,11 @@ 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/';
|
||||
import { token, user_name, pb_id } from '../lib/CookieValues';
|
||||
import CommentSystem from './CommentSystem/CommentSystem';
|
||||
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||
export default function TopicDetail(props) {
|
||||
// console.log('coockie data', user_name)
|
||||
const [showCopied, setShowCopied] = useState(false);
|
||||
|
||||
if (!props.topic) {
|
||||
|
@ -111,7 +113,7 @@ export default function TopicDetail(props) {
|
|||
|
||||
{/* Enhanced Social Share Buttons */}
|
||||
<div className="mb-8">
|
||||
<p className="text-sm text-gray-500 mb-3 font-medium">Share this article:</p>
|
||||
<p className="text-sm text-gray-500 mb-3 font-medium">Share this Topic:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => shareOnSocialMedia('facebook')}
|
||||
|
@ -193,7 +195,7 @@ export default function TopicDetail(props) {
|
|||
</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}/>
|
||||
<CommentSystem topicId={props.topic.id}/>
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||
import { CustomTabs } from './ui/tabs';
|
||||
import Loader from "./ui/loader";
|
||||
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const slug = urlParams.get('slug');
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function TopicCreation() {
|
|||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`,
|
||||
`https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`,
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function TopicCreation() {
|
|||
const fetchTopics = async (page, search = '') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = `https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`;
|
||||
let url = `https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/?query=get-all-topics&page=${page}`;
|
||||
|
||||
if (search) {
|
||||
url += `&search=${encodeURIComponent(search)}`;
|
||||
|
|
|
@ -52,8 +52,8 @@ export default function ProfilePage() {
|
|||
const [toast, setToast] = useState<ToastState>({ visible: false, message: '' });
|
||||
const [txnId, setTxnId] = useState<string>('');
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
|
||||
const USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/';
|
||||
const INVOICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/invoice/';
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSessionData = async () => {
|
||||
|
@ -335,10 +335,10 @@ export default function ProfilePage() {
|
|||
<span className="font-bold">Amount:</span>
|
||||
<span>₹{selectedData?.amount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
{/* <div className="flex justify-between">
|
||||
<span className="font-bold">User:</span>
|
||||
<span>{selectedData?.user}</span>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="flex justify-between">
|
||||
<span className="font-bold">Status:</span>
|
||||
<span className={`px-2 py-0.5 rounded text-white text-xs ${selectedData?.status === 'pending' ? 'bg-yellow-500' : selectedData?.status === 'completed' ? 'bg-green-500' : 'bg-red-500'}`}>
|
||||
|
|
|
@ -2,10 +2,9 @@
|
|||
import '../styles/global.css';
|
||||
import LoginProfile from '../components/LoginOrProfile';
|
||||
|
||||
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; newSchema1 : string}
|
||||
|
||||
const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, type = "website", newSchema1} = Astro.props;
|
||||
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; keyWords?: string; newSchema1 : string; newSchema2 : string; newSchema3 : string; newSchema4 : string; newSchema5 : string}
|
||||
|
||||
const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, type = "website", keyWords, newSchema1, newSchema2, newSchema3, newSchema4, newSchema5} = Astro.props;
|
||||
// Organization schema
|
||||
|
||||
---
|
||||
|
@ -24,6 +23,7 @@ const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname,
|
|||
<meta name="keywords" content="hosting, PHP hosting, Node.js hosting, Python hosting, Kubernetes, K8s, K3s, cloud services, web hosting, application hosting, server hosting, India hosting" />
|
||||
<meta name="author" content="SiliconPin" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<meta name="keywords" content={keyWords} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content={type} />
|
||||
|
@ -46,6 +46,10 @@ const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname,
|
|||
|
||||
<!-- Structured Data -->
|
||||
<script is:inline type="application/ld+json" set:html={newSchema1}></script>
|
||||
<script is:inline type="application/ld+json" set:html={newSchema2}></script>
|
||||
<script is:inline type="application/ld+json" set:html={newSchema3}></script>
|
||||
<script is:inline type="application/ld+json" set:html={newSchema4}></script>
|
||||
<script is:inline type="application/ld+json" set:html={newSchema5}></script>
|
||||
|
||||
<!-- For markdown editor css -->
|
||||
<link rel="stylesheet" href="/assets/md-editor/mdeditor.css" />
|
||||
|
@ -232,6 +236,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
|
@ -277,4 +282,91 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* comment-system.css */
|
||||
.comment-system {
|
||||
/* max-width: 800px; */
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.comment-thread {
|
||||
/* margin-bottom: 1.5rem; */
|
||||
/* padding: 1rem; */
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #ddd;
|
||||
}
|
||||
|
||||
.comment-thread.reply {
|
||||
background: #f0f0f0;
|
||||
border-left-color: #aaa;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-bottom: 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reply-form {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
background: #fde0e0;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.new-comment-card {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-comments {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// CookieValues.js
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export const token = Cookies.get('token') || null;
|
||||
export const user_name = Cookies.get('user_name') || null;
|
||||
export const pb_id = Cookies.get('pb_id') || null;
|
||||
export const siliconId = Cookies.get('siliconId') || null;
|
||||
export const isLogin = Cookies.get('isLogin') || null;
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
|
||||
const USER_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/users/index.php';
|
||||
|
||||
export const useIsLoggedIn = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(null); // null means "unknown"
|
||||
|
|
|
@ -17,7 +17,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
|||
<div class="text-center mb-8 sm:mb-12">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">About SiliconPin</h1>
|
||||
<p class="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
|
||||
Empowering businesses with reliable, high-performance hosting solutions
|
||||
We promote awareness about digital freedom by provoding software and hardware development and research
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,55 +3,116 @@ import Layout from '../layouts/Layout.astro';
|
|||
import { ContactForm } from '../components/ContactForm';
|
||||
|
||||
// Page-specific SEO metadata
|
||||
const pageTitle = "Contact Us | SiliconPin";
|
||||
const pageDescription = "Contact SiliconPin for reliable hosting solutions. Our team is available 24/7 to help with your PHP, Node.js, Python, Kubernetes, and K3s hosting needs.";
|
||||
const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||
const metaTitle = "Contact Us | SiliconPin";
|
||||
const metaDescription = "Contact SiliconPin for reliable hosting solutions. Our team is available 24/7 to help with your PHP, Node.js, Python, Kubernetes, and K3s hosting needs.";
|
||||
const ogImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=80&w=2000&auto=format&fit=crop";
|
||||
|
||||
// Contact schema for structured data
|
||||
const contactSchema = {
|
||||
const contactSchema1 = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ContactPage",
|
||||
"name": "SiliconPin Contact Page",
|
||||
"description": "Contact SiliconPin for reliable hosting solutions",
|
||||
"url": "https://siliconpin.com/contact",
|
||||
"mainEntityOfPage": {
|
||||
"name": "Contact SiliconPin",
|
||||
"url": "https://siliconpin.com/contact/",
|
||||
"description": "Contact page for SiliconPin – Reach out for cloud hosting, VPN, DevOps, and AI services.",
|
||||
"inLanguage": ["en", "hi", "bn"],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
const contactSchema2 = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "SiliconPin",
|
||||
"url": "https://siliconpin.com",
|
||||
"logo": "https://siliconpin.com/images/logo.png",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+91-700-160-1485",
|
||||
"contactType": "Customer Support",
|
||||
"areaServed": "IN",
|
||||
"availableLanguage": ["English", "Hindi", "Bengali"]
|
||||
},
|
||||
"sameAs": [
|
||||
"https://www.linkedin.com/company/siliconpin",
|
||||
"https://x.com/dwd_consultancy",
|
||||
"https://www.facebook.com/dwdsiliconpin",
|
||||
"https://instagram.com/siliconpin.com_"
|
||||
],
|
||||
"openingHoursSpecification": [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": "Monday",
|
||||
"opens": "00:00",
|
||||
"closes": "00:00"
|
||||
},
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": [
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday"
|
||||
],
|
||||
"opens": "09:00",
|
||||
"closes": "17:00"
|
||||
},
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": "Saturday",
|
||||
"opens": "10:00",
|
||||
"closes": "16:00"
|
||||
},
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": "Sunday",
|
||||
"opens": "00:00",
|
||||
"closecs": "00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const contactSchema3 = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Contact Us - SiliconPin",
|
||||
"url": "https://siliconpin.com/contact/",
|
||||
"description": "Contact the SiliconPin team for cloud hosting, VPN, AI agents, and DevOps services. We’re here to help developers, startups, and businesses.",
|
||||
"inLanguage": ["en", "hi", "bn"],
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "SiliconPin",
|
||||
"url": "https://siliconpin.com"
|
||||
},
|
||||
"about": {
|
||||
"@type": "Organization",
|
||||
"name": "SiliconPin",
|
||||
"telephone": "+91-700-160-1485",
|
||||
"email": "contact@siliconpin.com",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "121 Lalbari, GourBongo Road",
|
||||
"addressLocality": "Habra",
|
||||
"addressRegion": "W.B.",
|
||||
"postalCode": "743271",
|
||||
"addressCountry": "India"
|
||||
},
|
||||
"openingHoursSpecification": [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
"opens": "09:00",
|
||||
"closes": "18:00"
|
||||
},
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
"dayOfWeek": "Saturday",
|
||||
"opens": "10:00",
|
||||
"closes": "16:00"
|
||||
}
|
||||
]
|
||||
"url": "https://siliconpin.com"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={pageTitle}
|
||||
description={pageDescription}
|
||||
ogImage={pageImage}
|
||||
title={metaTitle}
|
||||
description={metaDescription}
|
||||
ogImage={ogImage}
|
||||
type="website"
|
||||
>
|
||||
<script type="application/ld+json" set:html={JSON.stringify(contactSchema)}></script>
|
||||
newSchema1={JSON.stringify(contactSchema1)}
|
||||
newSchema2={JSON.stringify(contactSchema2)}
|
||||
newSchema3={JSON.stringify(contactSchema3)}
|
||||
>
|
||||
|
||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||
<div class="text-center mb-8 sm:mb-12">
|
||||
|
|
|
@ -1,12 +1,52 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const schema1 = {
|
||||
const schema1 ={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Service",
|
||||
"name": "Cloud Hosting, VPN, DevOps & AI Services",
|
||||
"description": "SiliconPin provides cloud hosting, secure VPN, AI tools, and DevOps services tailored for developers, startups, and small businesses.",
|
||||
"serviceType": "Cloud Hosting, VPN, DevOps, AI Tools",
|
||||
"provider": {
|
||||
"@type": "Organization",
|
||||
"name": "SiliconPin",
|
||||
"url": "https://siliconpin.com",
|
||||
"logo": "https://siliconpin.com/images/logo.png",
|
||||
"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_"
|
||||
]
|
||||
},
|
||||
"areaServed": {
|
||||
"@type": "Place",
|
||||
"name": "Worldwide"
|
||||
},
|
||||
"url": "https://siliconpin.com/services/"
|
||||
}
|
||||
|
||||
const schema2 ={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "SiliconPin",
|
||||
"url": "https://siliconpin.com",
|
||||
"logo": "https://siliconpin.com/assets/logo.svg",
|
||||
"logo": "https://siliconpin.com/images/logo.png",
|
||||
"description": "SiliconPin delivers affordable and scalable cloud infrastructure, VPN services, AI tools, and DevOps for modern developers and businesses.",
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+91-700-160-1485",
|
||||
|
@ -27,19 +67,46 @@ const schema1 = {
|
|||
"https://www.facebook.com/dwdsiliconpin",
|
||||
"https://instagram.com/siliconpin.com_"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const schema3 ={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Our Services",
|
||||
"url": "https://siliconpin.com/services/",
|
||||
"description": "Explore SiliconPin’s cloud hosting, VPN, AI tools, and DevOps services built for developers, startups, and small businesses.",
|
||||
"inLanguage": ["en", "hi", "bn"],
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "SiliconPin",
|
||||
"url": "https://siliconpin.com"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "SiliconPin",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://siliconpin.com/images/logo.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page-specific SEO metadata
|
||||
const pageTitle = "SiliconPin - Lets create some digital freedom";
|
||||
const pageDescription = "SiliconPin - easy to deploy apps and tools, freedom oriented apps and tools, high-performance, hosting solutions for PHP, Node.js, Python, Kubernetes (K8s), and K3s, and technical support.";
|
||||
const pageImage = "/assets/logo.svg";
|
||||
const ogImage = "/assets/logo.svg";
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={pageTitle}
|
||||
description={pageDescription}
|
||||
ogImage={pageImage}
|
||||
ogImage={ogImage}
|
||||
keyWords={'here is keywords content'}
|
||||
newSchema1={JSON.stringify(schema1)}
|
||||
newSchema2={JSON.stringify(schema2)}
|
||||
newSchema3={JSON.stringify(schema3)}
|
||||
newSchema4={'here is another schema content'}
|
||||
newSchema5={'here is another schema content'}
|
||||
|
||||
>
|
||||
<!-- Canvas background animation -->
|
||||
|
|
|
@ -69,7 +69,7 @@ const services = [
|
|||
buyButtonUrl: ''
|
||||
}
|
||||
];
|
||||
const SERVICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/services/?query=all-services-list';
|
||||
const SERVICE_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/services/?query=all-services-list';
|
||||
let data = [];
|
||||
try {
|
||||
const response = await fetch(SERVICE_API_URL);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Layout from "../../layouts/Layout.astro";
|
||||
import TopicDetail from "../../components/TopicDetail";
|
||||
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
const { id } = Astro.params;
|
||||
|
||||
let topic = null;
|
||||
|
@ -25,7 +25,7 @@ try {
|
|||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${TOPIC_API_URL}?query=get-all-topics-for-slug`, {
|
||||
|
@ -48,8 +48,11 @@ export async function getStaticPaths() {
|
|||
// console.log(topic)
|
||||
---
|
||||
|
||||
<Layout title={topic?.title ?? 'Topic | SiliconPin'} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
|
||||
<Layout title={`${topic.title ? topic.title + '- Silicon Topic' : 'Silicon Topic'}`} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
|
||||
<TopicDetail client:load topic={topic} />
|
||||
</Layout>
|
||||
<script>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- topic?.title ?? 'Silicon Topic | SiliconPin' -->
|
|
@ -2,7 +2,7 @@
|
|||
import Layout from "../../layouts/Layout.astro";
|
||||
import TopicsList from "../../components/Topics";
|
||||
|
||||
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
|
||||
const TOPIC_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/topics/';
|
||||
let topics = [];
|
||||
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout title="">
|
||||
<div class="container mx-auto px-4" id="stt-container">
|
||||
<h1>WAV Recorder (16-bit Mono 16kHz)</h1>
|
||||
|
||||
<div id="audioVisualizer"></div>
|
||||
|
||||
<button id="startBtn">Start Recording</button>
|
||||
<button id="stopBtn" disabled>Stop Recording</button>
|
||||
<button id="playBtn" disabled>Play Recording</button>
|
||||
<button id="downloadBtn" disabled>Download WAV</button>
|
||||
|
||||
<div id="status">Ready to record</div>
|
||||
</div>
|
||||
</Layout>
|
||||
<script is:inline>
|
||||
// DOM elements
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const playBtn = document.getElementById('playBtn');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const audioVisualizer = document.getElementById('audioVisualizer');
|
||||
const statusDisplay = document.getElementById('status');
|
||||
|
||||
// Audio variables
|
||||
let audioContext;
|
||||
let mediaStream;
|
||||
let processor;
|
||||
let recording = false;
|
||||
let audioChunks = [];
|
||||
let audioBlob;
|
||||
let audioUrl;
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
sampleRate: 16000, // 16kHz sample rate
|
||||
numChannels: 1, // Mono
|
||||
bitDepth: 16 // 16-bit
|
||||
};
|
||||
|
||||
// Start recording
|
||||
startBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
statusDisplay.textContent = "Requesting microphone access...";
|
||||
|
||||
// Get microphone access
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
// Create audio context with our desired sample rate
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||
sampleRate: config.sampleRate
|
||||
});
|
||||
|
||||
// Create a script processor node to process the audio
|
||||
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
processor.onaudioprocess = processAudio;
|
||||
|
||||
// Create a media stream source
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
|
||||
// Connect the source to the processor and to the destination
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
|
||||
// Setup visualization
|
||||
setupVisualizer(source);
|
||||
|
||||
// Start recording
|
||||
recording = true;
|
||||
audioChunks = [];
|
||||
|
||||
// Update UI
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
statusDisplay.textContent = "Recording...";
|
||||
|
||||
console.log('Recording started at ' + config.sampleRate + 'Hz');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
statusDisplay.textContent = "Error: " + error.message;
|
||||
}
|
||||
});
|
||||
|
||||
// Process audio data
|
||||
function processAudio(event) {
|
||||
if (!recording) return;
|
||||
|
||||
// Get audio data (already mono because we set numChannels to 1)
|
||||
const inputData = event.inputBuffer.getChannelData(0);
|
||||
|
||||
// Convert float32 to 16-bit PCM
|
||||
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
|
||||
const s = Math.max(-1, Math.min(1, inputData[i]));
|
||||
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
|
||||
// Store the chunk
|
||||
audioChunks.push(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
// Stop recording
|
||||
stopBtn.addEventListener('click', () => {
|
||||
if (!recording) return;
|
||||
|
||||
// Stop recording
|
||||
recording = false;
|
||||
|
||||
// Disconnect processor
|
||||
if (processor) {
|
||||
processor.disconnect();
|
||||
processor = null;
|
||||
}
|
||||
|
||||
// Stop media stream tracks
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Create WAV file from collected chunks
|
||||
createWavFile();
|
||||
|
||||
// Update UI
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
playBtn.disabled = false;
|
||||
downloadBtn.disabled = false;
|
||||
statusDisplay.textContent = "Recording stopped. Ready to play or download.";
|
||||
});
|
||||
|
||||
// Create WAV file from collected PCM data
|
||||
function createWavFile() {
|
||||
// Combine all chunks into a single buffer
|
||||
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
const result = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
|
||||
audioChunks.forEach(chunk => {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
});
|
||||
|
||||
// Create WAV header
|
||||
const wavHeader = createWaveHeader(result.length, config);
|
||||
|
||||
// Combine header and PCM data
|
||||
const wavData = new Uint8Array(wavHeader.length + result.length);
|
||||
wavData.set(wavHeader, 0);
|
||||
wavData.set(result, wavHeader.length);
|
||||
|
||||
// Create blob
|
||||
audioBlob = new Blob([wavData], { type: 'audio/wav' });
|
||||
audioUrl = URL.createObjectURL(audioBlob);
|
||||
}
|
||||
|
||||
// Create WAV header
|
||||
function createWaveHeader(dataLength, config) {
|
||||
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||
|
||||
const buffer = new ArrayBuffer(44);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// RIFF identifier
|
||||
writeString(view, 0, 'RIFF');
|
||||
// RIFF chunk length
|
||||
view.setUint32(4, 36 + dataLength, true);
|
||||
// RIFF type
|
||||
writeString(view, 8, 'WAVE');
|
||||
// Format chunk identifier
|
||||
writeString(view, 12, 'fmt ');
|
||||
// Format chunk length
|
||||
view.setUint32(16, 16, true);
|
||||
// Sample format (raw)
|
||||
view.setUint16(20, 1, true);
|
||||
// Channel count
|
||||
view.setUint16(22, config.numChannels, true);
|
||||
// Sample rate
|
||||
view.setUint32(24, config.sampleRate, true);
|
||||
// Byte rate (sample rate * block align)
|
||||
view.setUint32(28, byteRate, true);
|
||||
// Block align (channel count * bytes per sample)
|
||||
view.setUint16(32, blockAlign, true);
|
||||
// Bits per sample
|
||||
view.setUint16(34, config.bitDepth, true);
|
||||
// Data chunk identifier
|
||||
writeString(view, 36, 'data');
|
||||
// Data chunk length
|
||||
view.setUint32(40, dataLength, true);
|
||||
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
// Helper to write strings to DataView
|
||||
function writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Play recorded audio
|
||||
playBtn.addEventListener('click', () => {
|
||||
if (!audioUrl) return;
|
||||
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.play();
|
||||
statusDisplay.textContent = "Playing recording...";
|
||||
|
||||
audio.onended = () => {
|
||||
statusDisplay.textContent = "Playback finished";
|
||||
};
|
||||
});
|
||||
|
||||
// Download recorded audio as WAV file
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
if (!audioBlob) return;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = audioUrl;
|
||||
a.download = `recording_${config.sampleRate}Hz_${config.bitDepth}bit.wav`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
statusDisplay.textContent = "Download started";
|
||||
});
|
||||
|
||||
// Setup audio visualization
|
||||
function setupVisualizer(source) {
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 64;
|
||||
source.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = audioVisualizer.offsetWidth;
|
||||
canvas.height = audioVisualizer.offsetHeight;
|
||||
audioVisualizer.innerHTML = '';
|
||||
audioVisualizer.appendChild(canvas);
|
||||
|
||||
const canvasCtx = canvas.getContext('2d');
|
||||
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
|
||||
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const barHeight = dataArray[i] / 2;
|
||||
|
||||
canvasCtx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
|
||||
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#stt-container > button {
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#audioVisualizer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#status {
|
||||
margin-top: 20px;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import AudioToText from "../../components/Tools/AudioToText"
|
||||
---
|
||||
|
||||
<Layout title="">
|
||||
|
||||
<AudioToText client:load />
|
||||
</Layout>
|
||||
<!-- <div class="container mx-auto px-4" id="stt-container">
|
||||
<h1 class="text-2xl font-bold mb-4">Audio File to STT</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2" for="audioFile">
|
||||
Upload Audio File (WAV format recommended)
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="audioFile"
|
||||
accept="audio/*,.wav"
|
||||
class="block w-full text-sm text-gray-500
|
||||
file:mr-4 file:py-2 file:px-4
|
||||
file:rounded-md file:border-0
|
||||
file:text-sm file:font-semibold
|
||||
file:bg-blue-50 file:text-blue-700
|
||||
hover:file:bg-blue-100"
|
||||
>
|
||||
<p class="mt-1 text-sm text-gray-500">Supported formats: WAV, MP3, OGG, etc.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="sendBtn"
|
||||
disabled
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send to STT API
|
||||
</button>
|
||||
|
||||
<div class="mt-6">
|
||||
<h2 class="text-xl font-semibold mb-2">API Response</h2>
|
||||
<div id="responseContainer" class="p-4 bg-gray-100 rounded-md min-h-20">
|
||||
<p id="statusMessage" class="text-gray-600">No file uploaded yet</p>
|
||||
<pre id="apiResponse" class="hidden mt-2 p-2 bg-white rounded overflow-auto"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <script is:inline>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const audioFileInput = document.getElementById('audioFile');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const apiResponse = document.getElementById('apiResponse');
|
||||
const responseContainer = document.getElementById('responseContainer');
|
||||
|
||||
// Enable send button when a file is selected
|
||||
audioFileInput.addEventListener('change', () => {
|
||||
if (audioFileInput.files.length > 0) {
|
||||
sendBtn.disabled = false;
|
||||
statusMessage.textContent = `File selected: ${audioFileInput.files[0].name}`;
|
||||
} else {
|
||||
sendBtn.disabled = true;
|
||||
statusMessage.textContent = 'No file selected';
|
||||
}
|
||||
apiResponse.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Handle sending to STT API
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
if (audioFileInput.files.length === 0) return;
|
||||
|
||||
const file = audioFileInput.files[0];
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.textContent = 'Processing...';
|
||||
statusMessage.textContent = `Sending ${file.name} to STT API...`;
|
||||
apiResponse.classList.add('hidden');
|
||||
responseContainer.classList.remove('bg-red-50', 'bg-green-50');
|
||||
responseContainer.classList.add('bg-blue-50');
|
||||
|
||||
// Create FormData and append the file
|
||||
const formData = new FormData();
|
||||
formData.append('audio', file);
|
||||
|
||||
// Send to STT API
|
||||
const response = await fetch('https://stt-41.siliconpin.com/stt', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Display results
|
||||
apiResponse.textContent = JSON.stringify(result, null, 2);
|
||||
apiResponse.classList.remove('hidden');
|
||||
statusMessage.textContent = 'STT API Response:';
|
||||
responseContainer.classList.remove('bg-blue-50');
|
||||
responseContainer.classList.add('bg-green-50');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
statusMessage.textContent = `Error: ${error.message}`;
|
||||
responseContainer.classList.remove('bg-blue-50');
|
||||
responseContainer.classList.add('bg-red-50');
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.textContent = 'Send to STT API';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Additional styling if needed */
|
||||
#stt-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
#apiResponse {
|
||||
max-height: 300px;
|
||||
}
|
||||
</style> -->
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ImageLabelings from "../../components/Tools/ImageLabeling";
|
||||
---
|
||||
|
||||
<Layout title="">
|
||||
<ImageLabelings client:load />
|
||||
</Layout>
|
|
@ -0,0 +1,300 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WAV Recorder (16-bit Mono 16kHz)</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
margin: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#audioVisualizer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#status {
|
||||
margin-top: 20px;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WAV Recorder (16-bit Mono 16kHz)</h1>
|
||||
|
||||
<div id="audioVisualizer"></div>
|
||||
|
||||
<button id="startBtn">Start Recording</button>
|
||||
<button id="stopBtn" disabled>Stop Recording</button>
|
||||
<button id="playBtn" disabled>Play Recording</button>
|
||||
<button id="downloadBtn" disabled>Download WAV</button>
|
||||
|
||||
<div id="status">Ready to record</div>
|
||||
|
||||
<script>
|
||||
// DOM elements
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const playBtn = document.getElementById('playBtn');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const audioVisualizer = document.getElementById('audioVisualizer');
|
||||
const statusDisplay = document.getElementById('status');
|
||||
|
||||
// Audio variables
|
||||
let audioContext;
|
||||
let mediaStream;
|
||||
let processor;
|
||||
let recording = false;
|
||||
let audioChunks = [];
|
||||
let audioBlob;
|
||||
let audioUrl;
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
sampleRate: 16000, // 16kHz sample rate
|
||||
numChannels: 1, // Mono
|
||||
bitDepth: 16 // 16-bit
|
||||
};
|
||||
|
||||
// Start recording
|
||||
startBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
statusDisplay.textContent = "Requesting microphone access...";
|
||||
|
||||
// Get microphone access
|
||||
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
// Create audio context with our desired sample rate
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
||||
sampleRate: config.sampleRate
|
||||
});
|
||||
|
||||
// Create a script processor node to process the audio
|
||||
processor = audioContext.createScriptProcessor(4096, 1, 1);
|
||||
processor.onaudioprocess = processAudio;
|
||||
|
||||
// Create a media stream source
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
|
||||
// Connect the source to the processor and to the destination
|
||||
source.connect(processor);
|
||||
processor.connect(audioContext.destination);
|
||||
|
||||
// Setup visualization
|
||||
setupVisualizer(source);
|
||||
|
||||
// Start recording
|
||||
recording = true;
|
||||
audioChunks = [];
|
||||
|
||||
// Update UI
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
statusDisplay.textContent = "Recording...";
|
||||
|
||||
console.log('Recording started at ' + config.sampleRate + 'Hz');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
statusDisplay.textContent = "Error: " + error.message;
|
||||
}
|
||||
});
|
||||
|
||||
// Process audio data
|
||||
function processAudio(event) {
|
||||
if (!recording) return;
|
||||
|
||||
// Get audio data (already mono because we set numChannels to 1)
|
||||
const inputData = event.inputBuffer.getChannelData(0);
|
||||
|
||||
// Convert float32 to 16-bit PCM
|
||||
const buffer = new ArrayBuffer(inputData.length * 2);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
for (let i = 0, offset = 0; i < inputData.length; i++, offset += 2) {
|
||||
const s = Math.max(-1, Math.min(1, inputData[i]));
|
||||
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
|
||||
// Store the chunk
|
||||
audioChunks.push(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
// Stop recording
|
||||
stopBtn.addEventListener('click', () => {
|
||||
if (!recording) return;
|
||||
|
||||
// Stop recording
|
||||
recording = false;
|
||||
|
||||
// Disconnect processor
|
||||
if (processor) {
|
||||
processor.disconnect();
|
||||
processor = null;
|
||||
}
|
||||
|
||||
// Stop media stream tracks
|
||||
if (mediaStream) {
|
||||
mediaStream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Create WAV file from collected chunks
|
||||
createWavFile();
|
||||
|
||||
// Update UI
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
playBtn.disabled = false;
|
||||
downloadBtn.disabled = false;
|
||||
statusDisplay.textContent = "Recording stopped. Ready to play or download.";
|
||||
});
|
||||
|
||||
// Create WAV file from collected PCM data
|
||||
function createWavFile() {
|
||||
// Combine all chunks into a single buffer
|
||||
const length = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
const result = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
|
||||
audioChunks.forEach(chunk => {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
});
|
||||
|
||||
// Create WAV header
|
||||
const wavHeader = createWaveHeader(result.length, config);
|
||||
|
||||
// Combine header and PCM data
|
||||
const wavData = new Uint8Array(wavHeader.length + result.length);
|
||||
wavData.set(wavHeader, 0);
|
||||
wavData.set(result, wavHeader.length);
|
||||
|
||||
// Create blob
|
||||
audioBlob = new Blob([wavData], { type: 'audio/wav' });
|
||||
audioUrl = URL.createObjectURL(audioBlob);
|
||||
}
|
||||
|
||||
// Create WAV header
|
||||
function createWaveHeader(dataLength, config) {
|
||||
const byteRate = config.sampleRate * config.numChannels * (config.bitDepth / 8);
|
||||
const blockAlign = config.numChannels * (config.bitDepth / 8);
|
||||
|
||||
const buffer = new ArrayBuffer(44);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// RIFF identifier
|
||||
writeString(view, 0, 'RIFF');
|
||||
// RIFF chunk length
|
||||
view.setUint32(4, 36 + dataLength, true);
|
||||
// RIFF type
|
||||
writeString(view, 8, 'WAVE');
|
||||
// Format chunk identifier
|
||||
writeString(view, 12, 'fmt ');
|
||||
// Format chunk length
|
||||
view.setUint32(16, 16, true);
|
||||
// Sample format (raw)
|
||||
view.setUint16(20, 1, true);
|
||||
// Channel count
|
||||
view.setUint16(22, config.numChannels, true);
|
||||
// Sample rate
|
||||
view.setUint32(24, config.sampleRate, true);
|
||||
// Byte rate (sample rate * block align)
|
||||
view.setUint32(28, byteRate, true);
|
||||
// Block align (channel count * bytes per sample)
|
||||
view.setUint16(32, blockAlign, true);
|
||||
// Bits per sample
|
||||
view.setUint16(34, config.bitDepth, true);
|
||||
// Data chunk identifier
|
||||
writeString(view, 36, 'data');
|
||||
// Data chunk length
|
||||
view.setUint32(40, dataLength, true);
|
||||
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
// Helper to write strings to DataView
|
||||
function writeString(view, offset, string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Play recorded audio
|
||||
playBtn.addEventListener('click', () => {
|
||||
if (!audioUrl) return;
|
||||
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.play();
|
||||
statusDisplay.textContent = "Playing recording...";
|
||||
|
||||
audio.onended = () => {
|
||||
statusDisplay.textContent = "Playback finished";
|
||||
};
|
||||
});
|
||||
|
||||
// Download recorded audio as WAV file
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
if (!audioBlob) return;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = audioUrl;
|
||||
a.download = `recording_${config.sampleRate}Hz_${config.bitDepth}bit.wav`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
statusDisplay.textContent = "Download started";
|
||||
});
|
||||
|
||||
// Setup audio visualization
|
||||
function setupVisualizer(source) {
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 64;
|
||||
source.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = audioVisualizer.offsetWidth;
|
||||
canvas.height = audioVisualizer.offsetHeight;
|
||||
audioVisualizer.innerHTML = '';
|
||||
audioVisualizer.appendChild(canvas);
|
||||
|
||||
const canvasCtx = canvas.getContext('2d');
|
||||
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
|
||||
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const barWidth = (canvas.width / bufferLength) * 2.5;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const barHeight = dataArray[i] / 2;
|
||||
|
||||
canvasCtx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
|
||||
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
32
yarn.lock
32
yarn.lock
|
@ -2519,6 +2519,15 @@ fraction.js@^4.3.7:
|
|||
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
framer-motion@^12.18.1:
|
||||
version "12.18.1"
|
||||
resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-12.18.1.tgz"
|
||||
integrity sha512-6o4EDuRPLk4LSZ1kRnnEOurbQ86MklVk+Y1rFBUKiF+d2pCdvMjWVu0ZkyMVCTwl5UyTH2n/zJEJx+jvTYuxow==
|
||||
dependencies:
|
||||
motion-dom "^12.18.1"
|
||||
motion-utils "^12.18.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
fs-extra@^11.1.0:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz"
|
||||
|
@ -3378,6 +3387,11 @@ jiti@^1.21.6, jiti@>=1.21.0:
|
|||
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
|
||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||
|
||||
js-cookie@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz"
|
||||
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
|
@ -4554,6 +4568,18 @@ mkdirp@^2.1.6:
|
|||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz"
|
||||
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
||||
|
||||
motion-dom@^12.18.1:
|
||||
version "12.18.1"
|
||||
resolved "https://registry.npmjs.org/motion-dom/-/motion-dom-12.18.1.tgz"
|
||||
integrity sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==
|
||||
dependencies:
|
||||
motion-utils "^12.18.1"
|
||||
|
||||
motion-utils@^12.18.1:
|
||||
version "12.18.1"
|
||||
resolved "https://registry.npmjs.org/motion-utils/-/motion-utils-12.18.1.tgz"
|
||||
integrity sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
|
||||
|
@ -5117,7 +5143,7 @@ radix3@^1.1.2:
|
|||
resolved "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz"
|
||||
integrity sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==
|
||||
|
||||
"react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^17.0.2 || ^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=16.8.2, react-dom@>=18:
|
||||
"react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^17.0.2 || ^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=16.8.2, react-dom@>=18:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz"
|
||||
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
|
||||
|
@ -5245,7 +5271,7 @@ react-to-print@^3.0.5:
|
|||
resolved "https://registry.npmjs.org/react-to-print/-/react-to-print-3.0.5.tgz"
|
||||
integrity sha512-Z15MwMOzYCHWi26CZeFNwflAg7Nr8uWD6FTj+EkfIOjYyjr0MXGbI0c7rF4Fgrbj3XG9hFndb1ourxpPz2RAiA==
|
||||
|
||||
react@*, "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ~19", "react@^17.0.2 || ^18.0.0 || ^19.0.0", react@^19.0.0, react@>=16, react@>=16.8.0, react@>=16.8.2, react@>=18:
|
||||
react@*, "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ~19", "react@^17.0.2 || ^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0", react@^19.0.0, react@>=16, react@>=16.8.0, react@>=16.8.2, react@>=18:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
|
||||
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
|
||||
|
@ -6264,7 +6290,7 @@ tsconfig-paths@^4.2.0:
|
|||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.8.0:
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
|
Loading…
Reference in New Issue