pull/32/head
suvodip ghosh 2025-04-21 13:40:26 +00:00
parent 7258809230
commit 10a2c0c2c3
22 changed files with 11496 additions and 790 deletions

View File

@ -11,12 +11,12 @@ export default defineConfig({
},
preview: {
allowedHosts: ['siliconpin.cs1.hz.siliconpin.com'],
},
}
},
integrations: [tailwind(), react()],
server: {
host: '0.0.0.0',
port: 4000
port: 4000,
},
output: 'static',
});

8857
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,17 +19,25 @@
"@shadcn/ui": "^0.0.4",
"@types/date-fns": "^2.5.3",
"@types/react": "^19.0.12",
"@uiw/react-md-editor": "^3.25.6",
"astro": "^5.5.2",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.484.0",
"marked": "^15.0.8",
"minio": "^8.0.5",
"pocketbase": "^0.25.2",
"postcss": "^8.5.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-qr-code": "^2.0.15",
"react-router-dom": "^7.5.0",
"react-router-dom": "^7.5.1",
"react-simplemde-editor": "^5.2.0",
"react-to-print": "^3.0.5",
"rehype-rewrite": "^4.0.2",
"simplemde": "^1.11.2",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7"

View File

@ -54,29 +54,12 @@ export function ContactForm() {
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="name" className="text-sm sm:text-base">Name</Label>
<Input
id="name"
name="name"
placeholder="Your name"
value={formState.name}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
<Input id="name" name="name" placeholder="Your name" value={formState.name} onChange={handleChange} required className="text-sm sm:text-base" />
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="email" className="text-sm sm:text-base">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="your.email@example.com"
value={formState.email}
onChange={handleChange}
required
className="text-sm sm:text-base"
/>
<Input id="email" name="email" type="email" placeholder="your.email@example.com" value={formState.email} onChange={handleChange} required className="text-sm sm:text-base" />
</div>
</div>
@ -84,26 +67,12 @@ export function ContactForm() {
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4">
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="company" className="text-sm sm:text-base">Company</Label>
<Input
id="company"
name="company"
placeholder="Your company name"
value={formState.company}
onChange={handleChange}
className="text-sm sm:text-base"
/>
<Input id="company" name="company" placeholder="Your company name" value={formState.company} onChange={handleChange} className="text-sm sm:text-base" />
</div>
<div className="space-y-1 sm:space-y-2">
<Label htmlFor="service" className="text-sm sm:text-base">Service Interest</Label>
<Select
id="service"
name="service"
value={formState.service}
onChange={handleChange}
required
className="text-sm sm:text-base"
>
<Select id="service" name="service" value={formState.service} onChange={handleChange} required className="text-sm sm:text-base">
<option value="" disabled>Select a service</option>
<option value="php">PHP Hosting</option>
<option value="nodejs">Node.js Hosting</option>

View File

@ -7,7 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Separator } from "./ui/separator";
import { Label } from "./ui/label";
import { useIsLoggedIn } from '../lib/isLoggedIn';
interface AuthStatus {
message: string;
isError: boolean;
@ -144,7 +144,16 @@ const LoginPage = () => {
throw error; // Re-throw the error if you want calling functions to handle it
}
};
const { isLoggedIn, loading, error } = useIsLoggedIn();
// console.log(isLoggedIn)
if(loading){
return <p>Loading...</p>
}
if(isLoggedIn){
return (
window.location.href = '/'
)
}
return (
<div className="min-h-screen flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg rounded-xl overflow-hidden">

View File

@ -0,0 +1,59 @@
import React, { useEffect, useState } from "react";
import TopicItems from "./TopicItem";
import { useIsLoggedIn } from '../lib/isLoggedIn';
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation(props) {
const { isLoggedIn, loading: authLoading, error: authError } = useIsLoggedIn();
const [topics, setTopics] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchTopics = async () => {
try {
const res = await fetch('https://host-api.cs1.hz.siliconpin.com/v1/topics/?query=my-topics', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
setTopics(data.data || []);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchTopics();
}, []);
if (authLoading || loading) {
return <div className="loading-indicator">Loading...</div>;
}
if (authError || error) {
return <div className="error-message">Error loading data: {authError || error}</div>;
}
return (
<>
{isLoggedIn && (
<div className="container mx-auto flex justify-end gap-x-4 mb-4">
<a href="/topic/new" className="create-new-link">Create New</a>
<a href="/topic/my-topic">My Topics</a>
</div>
)}
<TopicItems topics={topics} mytopic={props.mytopic || false} title="SoliconPin Topics" description={topicPageDesc} />
</>
);
}

484
src/components/NewTopic.jsx Normal file
View File

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

View File

@ -1,8 +1,9 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { marked } from 'marked';
export default function TopicDetail(props) {
console.log('All topic Form allTopic', props.allTopic)
// console.log('All topic Form allTopic', props.allTopic);
if (!props.topic) {
return <div>Topic not found</div>;
}
@ -12,7 +13,7 @@ console.log('All topic Form allTopic', props.allTopic)
<article className="max-w-4xl mx-auto">
<img src={props.topic.img} alt={props.topic.title} className="w-full h-96 object-cover rounded-lg mb-8" />
<h1 className="text-4xl font-bold text-[#6d9e37] mb-4">{props.topic.title}</h1>
<p className="font-light mb-8 text-justify" dangerouslySetInnerHTML={{__html: props.topic.content}}></p>
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}></div>
</article>
</div>
);

View File

@ -0,0 +1,528 @@
import React, { useState, useEffect, useRef } from "react";
import MDEditor, { commands } from '@uiw/react-md-editor';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from './ui/select';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { CustomTabs } from './ui/tabs';
const TOPIC_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/topics/';
const MINIO_UPLOAD_URL = 'https://your-minio-api-endpoint/upload';
const urlParams = new URLSearchParams(window.location.search);
const slug = urlParams.get('slug');
// console.log('find slug from url', slug);
export default function EditTopic (){
// const { slug } = useParams();
// const navigate = useNavigate();
// Form state
const [formData, setFormData] = useState({ status: 'draft', category: '', slug: '', title: '', content: '', imageUrl: '' });
// UI state
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const [imageFile, setImageFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [imageDialogOpen, setImageDialogOpen] = useState(false);
const [imageUrlInput, setImageUrlInput] = useState('');
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
const [editorMode, setEditorMode] = useState('edit');
// Fetch topic data on component mount
useEffect(() => {
const fetchTopic = async () => {
try {
const response = await fetch(`${TOPIC_API_URL}?query=get-single-topic&slug=${slug}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch topic');
}
const data = await response.json();
const topic = data.data[0]
console.log('Single topic data', data)
setFormData({
status: topic.status || 'draft',
category: topic.category || '',
title: topic.title || '',
slug: topic.slug || '',
content: topic.content || '',
imageUrl: topic.imageUrl || ''
});
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchTopic();
}, [slug]);
// Upload file to MinIO (same as NewTopic)
const uploadToMinIO = async (file, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('bucket', 'siliconpin-uploads');
formData.append('folder', 'topic-images');
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.url;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
// Generate slug from title (same as NewTopic)
// useEffect(() => {
// if (formData.title) {
// const newSlug = formData.title
// .toLowerCase()
// .replace(/[^\w\s]/g, '')
// .replace(/\s+/g, '-');
// setFormData(prev => ({ ...prev, slug: newSlug }));
// }
// }, [formData.title]);
// Custom image command for MDEditor (same as NewTopic)
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 commands (same as NewTopic)
const allCommands = commands.getCommands().map(cmd => {
if (cmd.name === 'image') {
return customImageCommand;
}
return cmd;
});
// Handle image URL insertion (same as NewTopic)
const handleInsertImageUrl = () => {
if (imageUrlInput) {
const imgMarkdown = `![Image](${imageUrlInput})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUrlInput('');
}
};
// Handle image file selection (same as NewTopic)
const handleImageFileSelect = (e) => {
const file = e.target.files[0];
if (file) {
setImageUploadFile(file);
const reader = new FileReader();
reader.onload = () => {
setImageUploadPreview(reader.result);
};
reader.readAsDataURL(file);
}
};
// Upload image file (same as NewTopic)
const handleImageUpload = async () => {
if (!imageUploadFile) return;
try {
setIsSubmitting(true);
setUploadProgress(0);
const uploadedUrl = await uploadToMinIO(imageUploadFile, (progress) => {
setUploadProgress(progress);
});
const imgMarkdown = `![Image](${uploadedUrl})`;
const textarea = document.querySelector('.w-md-editor-text-input');
if (textarea) {
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = formData.content;
const newValue =
currentValue.substring(0, startPos) +
imgMarkdown +
currentValue.substring(endPos);
setFormData(prev => ({ ...prev, content: newValue }));
}
setImageDialogOpen(false);
setImageUploadFile(null);
setImageUploadPreview('');
} catch (error) {
setError('Failed to upload image: ' + error.message);
} finally {
setIsSubmitting(false);
}
};
// Form submission for EDIT
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
setUploadProgress(0);
try {
// Upload new featured image if selected
let imageUrl = formData.imageUrl;
if (imageFile) {
imageUrl = await uploadToMinIO(imageFile, (progress) => {
setUploadProgress(progress);
});
}
// Prepare payload
const payload = {
...formData,
imageUrl
};
// Submit to API (PUT request for update)
const response = await fetch(`${TOPIC_API_URL}?query=update-topic&slug=${formData.slug}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Failed to update topic');
}
setSuccess(true);
setTimeout(() => {
navigate(`/topic/${formData.slug}`);
}, 1500);
} catch (err) {
setError(err.message);
} finally {
setIsSubmitting(false);
setUploadProgress(0);
}
};
// Handle form field changes (same as NewTopic)
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
// Handle featured image file selection (same as NewTopic)
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
const reader = new FileReader();
reader.onload = () => {
setFormData(prev => ({ ...prev, imageUrl: reader.result }));
};
reader.readAsDataURL(file);
}
};
// Loading state
if (isLoading) {
return (
<div className="container mx-auto px-4 py-8 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#6d9e37] mx-auto"></div>
<p className="mt-4 text-gray-600">Topic Loading...</p>
</div>
);
}
// Success state
if (success) {
return (
<div className="container mx-auto px-4 text-center py-8">
<h3 className="text-xl font-semibold text-green-600 mb-4">Topic has been successfully updated!</h3>
<p className="mb-4">You are being automatically redirected to the topic page...</p>
<div className="flex justify-center gap-4">
<Button onClick={() => navigate(`/topic/${formData.slug}`)}>Preview this Topic</Button>
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-3xl mx-auto bg-neutral-800 rounded-lg shadow-md p-6">
<h2 className="text-2xl font-bold text-[#6d9e37] mb-2">Edit Topic</h2>
<p className="text-gray-600 mb-6">{formData.title}</p>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
name="status"
required
value={formData.status}
onValueChange={(value) => setFormData(prev => ({ ...prev, status: value }))}
>
<SelectTrigger id="status" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="published">Publish</SelectItem>
<SelectItem value="archived">Archive</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category">Category *</Label>
<Select
name="category"
required
value={formData.category}
onValueChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
>
<SelectTrigger id="category" className="text-sm sm:text-base w-full">
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent position="popper" className="z-50">
<SelectItem value="php">PHP Hosting</SelectItem>
<SelectItem value="nodejs">Node.js Hosting</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Title and Slug */}
<div className="space-y-2">
<Label htmlFor="title">Title *</Label>
<Input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Wriet Topic Title"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="slug">URL Slug</Label>
<input
type="hidden"
name="slug"
value={formData.slug}
onChange={handleChange}
className="bg-gray-50"
/>
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
</div>
{/* Content Editor */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="content">Content *</Label>
<button
type="button"
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
className={`ml-2 ${editorMode !== 'edit' ? 'bg-[#6d9e37]' : ''} text-white border border-[#6d9e37] text-[#6d9e37] px-2 py-1 rounded-md`}
>
{editorMode === 'edit' ? 'Preview' : 'Edit'}
</button>
</div>
<div data-color-mode="light">
<MDEditor
value={formData.content}
onChange={(value) => setFormData(prev => ({ ...prev, content: value || '' }))}
height={400}
preview={editorMode}
commands={allCommands}
/>
</div>
</div>
{/* Image Upload Dialog (same as NewTopic) */}
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add Image</DialogTitle>
</DialogHeader>
<CustomTabs
tabs={[
{
label: "URL",
value: "url",
content: (
<div className="space-y-4">
<Input
type="text"
placeholder="Image URL"
value={imageUrlInput}
onChange={(e) => setImageUrlInput(e.target.value)}
/>
<div className="flex justify-end">
<Button
type="button"
onClick={handleInsertImageUrl}
disabled={!imageUrlInput}
>
Upload Image
</Button>
</div>
</div>
)
},
{
label: "Upload",
value: "upload",
content: (
<div className="space-y-4">
<Input
type="file"
accept="image/*"
onChange={handleImageFileSelect}
/>
{imageUploadPreview && (
<div className="mt-2">
<img
src={imageUploadPreview}
alt="Preview"
className="max-h-40 rounded-md border"
/>
</div>
)}
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
onClick={handleImageUpload}
disabled={!imageUploadFile || isSubmitting}
>
{isSubmitting ? 'Uploading...' : 'Upload'}
</Button>
</div>
</div>
)
}
]}
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
<Input
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
<p className="text-xs text-gray-500 mt-1">Current Image</p>
</div>
)}
</div>
<Separator />
{/* Form Actions */}
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => navigate(-1)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="min-w-32 bg-[#6d9e37] hover:bg-[#5a8a2a]"
>
{isSubmitting ? (
<span className="flex items-center gap-2">
<span className="animate-spin"></span>
Uploading...
</span>
) : 'Update'}
</Button>
</div>
{error && (
<div className="p-4 bg-red-50 text-red-600 rounded-md">
Error: {error}
</div>
)}
</form>
</div>
</div>
);
};

View File

@ -1,33 +1,75 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { Pencil, Trash2 } from "lucide-react"; // Import icons for better visual
export default function TopicItems(props) {
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
if (loading) {
return <div className="loading-indicator">Loading...</div>;
}
if (error) {
return <div className="error-message">Error loading authentication status</div>;
}
return (
<section className="container mx-auto px-4">
<div className="py-8 text-center">
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">
{props.title}
</h2>
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">
{props.description}
</p>
<h2 className="text-3xl sm:text-4xl font-bold text-[#6d9e37] mb-3 sm:mb-4">{props.title}</h2>
<p className="text-lg sm:text-xl max-w-3xl mx-auto text-neutral-300">{props.description}</p>
</div>
{
props.topics.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{props.topics.map((topic) => (
<a href={`/topic/${topic.slug}`} key={topic.id} className="hover:scale-[1.02] transition-transform duration-200">
<Card className="h-full flex flex-col">
<img src={topic.img} alt={topic.title} className="aspect-video object-cover rounded-t-lg" loading="lazy" />
{
props.topics.map((topic) => (
<a href={`/topic/${topic.slug}`} key={topic.id} className="hover:scale-[1.02] transition-transform duration-200 ">
<Card className="h-full flex flex-col group relative">
<div className="relative">
<img src={topic.img} alt={topic.title} className="aspect-video object-cover rounded-t-lg relative" loading="lazy" />
</div>
<CardContent className="flex-1 p-6">
<CardTitle className="mb-2">{topic.title}</CardTitle>
<CardDescription className="line-clamp-3">
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
<CardDescription className="line-clamp-3 mb-4">
{topic.description}
</CardDescription>
{
// console.log('props.mytopic', props.mytopic)
}
{isLoggedIn && sessionData.user_email === topic.user && props.mytopic === true && (
<div className="flex justify-end gap-2 mt-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<Button variant="outline" size="sm" className="gap-1"
onClick={(e) => {
e.preventDefault();
window.location.href = `/topic/edit?slug=${topic.slug}`
// Handle edit action
}}
>
<Pencil className="h-4 w-4" />Edit
</Button>
<Button size="sm" className="gap-1 bg-red-500 hover:bg-red-600"
onClick={(e) => {
e.preventDefault();
// Handle delete action
}}
>
<Trash2 className="h-4 w-4" />Delete
</Button>
</div>
)}
</CardContent>
</Card>
</a>
))}
))
}
</div>
) : (
<p className="text-center">No Topic Found <a href="/topic/new" className="text-[#6d9e37]"> Click Here</a> to Create Once</p>
)
}
</section>
);
}

View File

@ -1,10 +1,29 @@
import React, {useEffect, useState} from "react";
import React from "react";
import TopicItems from "./TopicItem";
let topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation(props){
return(
import { useIsLoggedIn } from '../lib/isLoggedIn';
const topicPageDesc = 'Cutting-edge discussions on tech, digital services, news, and digital freedom. Stay informed on AI, cybersecurity, privacy, and the future of innovation.';
export default function TopicCreation(props) {
const { isLoggedIn, loading, error } = useIsLoggedIn();
if (loading) {
return <div className="loading-indicator">Loading...</div>;
}
if (error) {
return <div className="error-message">Error loading authentication status</div>;
}
return (
<>
{isLoggedIn && (
<div className="container mx-auto flex justify-end gap-x-4">
<a href="/topic/new" className="create-new-link">Create New</a>
<a href="/topic/my-topic">My Topics</a>
</div>
)}
<TopicItems topics={props.topics} title="SoliconPin Topics" description={topicPageDesc} />
</>
)
);
}

View File

@ -25,7 +25,7 @@ export default function ProfilePage() {
const [userData, setUserData] = useState<UserData | null>(null);
const [invoiceList, setInvoiceList] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/';
const INVOICE_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/invoice/';
useEffect(() => {
const fetchSessionData = async () => {
@ -73,18 +73,29 @@ export default function ProfilePage() {
}
};
fetchSessionData();
getInvoiceListData();
}, []);
const sessionLogOut = () => {
fetch(`${USER_API_URL}?query=logout`, {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => {
if(data.success === true){
window.location.href = '/';
}
console.log('Logout Console', data.success);
})
}
if (error) {
return <div>Error: {error}</div>;
}
if (!userData) {
return <div>Loading profile data...</div>;
return (<div>Loading profile data...</div>);
}
return (
<div className="space-y-6 container mx-auto">
@ -148,8 +159,8 @@ export default function ProfilePage() {
</thead>
<tbody>
{
invoiceList.map((invoice) => (
<tr key={invoice.id}>
invoiceList.map((invoice, index) => (
<tr key={index}>
<td>{invoice.invoice_number}</td>
<td>{invoice.invoice_date}</td>
<td>{invoice.notes ? invoice.notes : ''}</td>
@ -200,6 +211,7 @@ export default function ProfilePage() {
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-4">
<Button onClick={sessionLogOut} className="bg-red-500 hover:bg-red-600">Logout</Button>
<Button className="bg-red-500 hover:bg-red-600">Delete account</Button>
<Button variant="outline">Export data</Button>
</div>

View File

@ -0,0 +1,137 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "../../lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef(
({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
position === "popper" && "translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef(
({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
)
);
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef(
({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
"focus:bg-neutral-700 focus:text-white",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef(
({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
{...props}
/>
)
);
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
const ChevronDownIcon = React.forwardRef((props, ref) => (
<svg
ref={ref}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
));
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};
export {
Select as SelectProps,
SelectGroup as SelectGroupProps,
SelectValue as SelectValueProps,
SelectTrigger as SelectTriggerProps,
SelectContent as SelectContentProps,
SelectLabel as SelectLabelProps,
SelectItem as SelectItemProps,
SelectSeparator as SelectSeparatorProps,
};

View File

@ -1,130 +0,0 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "../../lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm",
"focus:outline-none focus:ring-2 focus:ring-[#6d9e37] focus:ring-offset-2 focus:ring-offset-neutral-900",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-neutral-600 bg-neutral-800 shadow-md animate-in fade-in-80",
className
)}
{...props}
>
<SelectPrimitive.Viewport className="p-1">
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none",
"focus:bg-neutral-700 focus:text-white",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-neutral-600", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
// ChevronDownIcon component
const ChevronDownIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => (
<svg
ref={ref}
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
));
ChevronDownIcon.displayName = "ChevronDownIcon";
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};

View File

@ -1,33 +1,48 @@
import * as React from "react";
import * as Tabs from "@radix-ui/react-tabs";
import * as RadixTabs from "@radix-ui/react-tabs";
import { cn } from "../../lib/utils";
export interface CustomTabsProps {
// Main Tabs component
const Tabs = RadixTabs.Root;
// Subcomponents
const TabsList = RadixTabs.List;
const TabsTrigger = RadixTabs.Trigger;
const TabsContent = RadixTabs.Content;
// CustomTabs component (your existing implementation)
interface CustomTabsProps {
tabs: { label: string; value: string; content: React.ReactNode }[];
className?: string;
}
const CustomTabs: React.FC<CustomTabsProps> = ({ tabs, className }) => {
return (
<Tabs.Root defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
<Tabs.List className="flex border-b border-neutral-600 bg-neutral-800">
<Tabs defaultValue={tabs[0]?.value} className={cn("w-full", className)}>
<TabsList className="flex border-b border-neutral-600 bg-neutral-800">
{tabs.map((tab) => (
<Tabs.Trigger
<TabsTrigger
key={tab.value}
value={tab.value}
className="px-4 py-2 text-sm text-neutral-400 hover:text-white focus-visible:ring-2 focus-visible:ring-[#6d9e37]"
>
{tab.label}
</Tabs.Trigger>
</TabsTrigger>
))}
</Tabs.List>
</TabsList>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="p-4 text-neutral-300">
<TabsContent key={tab.value} value={tab.value} className="p-4 text-neutral-300">
{tab.content}
</Tabs.Content>
</TabsContent>
))}
</Tabs.Root>
</Tabs>
);
};
export { CustomTabs };
export {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
CustomTabs
};

View File

@ -98,12 +98,8 @@ const organizationSchema = {
<a href="/services" class="hover:text-[#6d9e37] transition-colors">Services</a>
<a href="/topic" class="hover:text-[#6d9e37] transition-colors">Topic</a>
<a href="/contact" class="hover:text-[#6d9e37] transition-colors">Contact</a>
<a
href="/get-started"
class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors"
>
Get Started
</a>
<a href="/profile" class="hover:text-[#6d9e37] transition-colors">Profile</a>
<a href="/get-started" class="inline-flex items-center justify-center rounded-md bg-[#6d9e37] px-4 py-2 text-sm font-medium text-white hover:bg-[#598035] transition-colors">Get Started</a>
</nav>
<!-- Mobile menu button -->
<div class="md:hidden relative">
@ -140,9 +136,7 @@ const organizationSchema = {
</div>
<div class="hidden sm:block">
<a href="/suggestion-or-report" class="text-[#6d9e37] hover:text-white transition-colors">
Suggestion or Report
</a>
<a href="/suggestion-or-report" class="text-[#6d9e37] hover:text-white transition-colors">Suggestion or Report</a>
</div>
<div class="flex gap-5 mt-4 sm:mt-0">
@ -237,12 +231,12 @@ const organizationSchema = {
<span class="text-xs mt-1">Start</span>
</a>
<a href="/about-us" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
<svg width="20px" height="20px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#6d9e37"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>about</title> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="about-white" fill="#6d9e37" transform="translate(42.666667, 42.666667)"> <path d="M213.333333,3.55271368e-14 C95.51296,3.55271368e-14 3.55271368e-14,95.51168 3.55271368e-14,213.333333 C3.55271368e-14,331.153707 95.51296,426.666667 213.333333,426.666667 C331.154987,426.666667 426.666667,331.153707 426.666667,213.333333 C426.666667,95.51168 331.154987,3.55271368e-14 213.333333,3.55271368e-14 Z M213.333333,384 C119.227947,384 42.6666667,307.43872 42.6666667,213.333333 C42.6666667,119.227947 119.227947,42.6666667 213.333333,42.6666667 C307.44,42.6666667 384,119.227947 384,213.333333 C384,307.43872 307.44,384 213.333333,384 Z M240.04672,128 C240.04672,143.46752 228.785067,154.666667 213.55008,154.666667 C197.698773,154.666667 186.713387,143.46752 186.713387,127.704107 C186.713387,112.5536 197.99616,101.333333 213.55008,101.333333 C228.785067,101.333333 240.04672,112.5536 240.04672,128 Z M192.04672,192 L234.713387,192 L234.713387,320 L192.04672,320 L192.04672,192 Z" id="Shape"> </path> </g> </g> </g></svg>
<span class="text-xs mt-1">About</span>
</a>
<a href="/contact" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
<span class="text-xs mt-1">Contact</span>
<a href="/profile" class="flex flex-col items-center justify-center w-full h-full text-[#6d9e37] hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="10" r="3"></circle><path d="M7 20.662V19c0-1.18.822-2.2 2-2.5a5.5 5.5 0 0 1 6 0c1.178.3 2 1.323 2 2.5v1.662"></path></svg>
<span class="text-xs mt-1">Profile</span>
</a>
</div>
</nav>

44
src/lib/isLoggedIn.jsx Normal file
View File

@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';
const USER_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/users/index.php';
export const useIsLoggedIn = () => {
const [isLoggedIn, setIsLoggedIn] = useState(null); // null means "unknown"
const [sessionData, setSessionData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const checkLoginStatus = async () => {
try {
const response = await fetch(`${USER_API_URL}?query=isLoggedIn`, {
credentials: 'include'
});
const data = await response.json();
setIsLoggedIn(!!data?.isLoggedIn);
setSessionData(data?.session_data || null);
setLoading(false);
} catch (err) {
setError(err);
setIsLoggedIn(false);
setSessionData(null);
setLoading(false);
}
};
checkLoginStatus();
}, []);
return { isLoggedIn, sessionData, loading, error };
};
const IsLoggedIn = ({ children, fallback = null }) => {
const { isLoggedIn, loading, error } = useIsLoggedIn();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error checking login status</div>;
return isLoggedIn ? children : fallback;
};
export default IsLoggedIn;

View File

@ -0,0 +1,7 @@
---
import Layout from "../../layouts/Layout.astro";
import EditTopic from "../../components/TopicEdit";
---
<Layout title="Edit Topic | SiliconPin" >
<EditTopic client:load client:only="react" />
</Layout>

View File

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

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro";
import MyTopicList from "../../components/MyTopic";
---
<Layout title="Topics">
<MyTopicList client:load mytopic={true} />
</Layout>

View File

@ -0,0 +1,8 @@
---
import Layout from "../../layouts/Layout.astro";
import NewTopic from "../../components/NewTopic";
---
<Layout title="Create New Topic | Siliconpin">
<NewTopic client:load client:only="react" />
</Layout>

1739
yarn.lock

File diff suppressed because it is too large Load Diff