Compare commits
8 Commits
ee2bbc817f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cbe98eff5 | ||
| dffdbc6e40 | |||
| 0e030f0ce2 | |||
| 5cd02bfbde | |||
| 4b6e4af86d | |||
|
|
b58068d108 | ||
|
|
f9d1556ce9 | ||
| 0900d51954 |
80
package-lock.json
generated
80
package-lock.json
generated
@@ -12,16 +12,13 @@
|
|||||||
"@astrojs/tailwind": "^6.0.0",
|
"@astrojs/tailwind": "^6.0.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"@types/date-fns": "^2.5.3",
|
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"astro": "^5.5.2",
|
"astro": "^5.5.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"lucide-react": "^0.484.0",
|
"lucide-react": "^0.484.0",
|
||||||
"pocketbase": "^0.25.2",
|
"pocketbase": "^0.25.2",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
@@ -1739,37 +1736,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-roving-focus": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.1",
|
|
||||||
"@radix-ui/react-collection": "1.1.2",
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.1",
|
|
||||||
"@radix-ui/react-context": "1.1.1",
|
|
||||||
"@radix-ui/react-direction": "1.1.0",
|
|
||||||
"@radix-ui/react-id": "1.1.0",
|
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",
|
||||||
@@ -1831,36 +1797,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-tabs": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/primitive": "1.1.1",
|
|
||||||
"@radix-ui/react-context": "1.1.1",
|
|
||||||
"@radix-ui/react-direction": "1.1.0",
|
|
||||||
"@radix-ui/react-id": "1.1.0",
|
|
||||||
"@radix-ui/react-presence": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.0.2",
|
|
||||||
"@radix-ui/react-roving-focus": "1.1.2",
|
|
||||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||||
@@ -2463,12 +2399,6 @@
|
|||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/date-fns": {
|
|
||||||
"version": "2.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
|
|
||||||
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -3349,16 +3279,6 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
|||||||
@@ -14,16 +14,13 @@
|
|||||||
"@astrojs/tailwind": "^6.0.0",
|
"@astrojs/tailwind": "^6.0.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-select": "^2.1.6",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
"@types/date-fns": "^2.5.3",
|
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"astro": "^5.5.2",
|
"astro": "^5.5.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"lucide-react": "^0.484.0",
|
"lucide-react": "^0.484.0",
|
||||||
"pocketbase": "^0.25.2",
|
"pocketbase": "^0.25.2",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg width="15px" height="15px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M12 8V12L15 15" stroke="#6d9e37" stroke-width="2" stroke-linecap="round"></path> <circle cx="12" cy="12" r="9" stroke="#6d9e37" stroke-width="2"></circle> </g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 430 B |
@@ -1 +0,0 @@
|
|||||||
<svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M20 4L3 9.31372L10.5 13.5M20 4L14.5 21L10.5 13.5M20 4L10.5 13.5" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 446 B |
5
public/data-not-updated-or-not-found/index.php
Normal file
5
public/data-not-updated-or-not-found/index.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
echo 'Data not updated or the page not found!';
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -1,776 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Toast } from './Toast';
|
|
||||||
import { TemplatePreview } from './TemplatePreview';
|
|
||||||
|
|
||||||
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
|
||||||
// Deployment type and app selections
|
|
||||||
const [deploymentType, setDeploymentType] = useState('app');
|
|
||||||
const [appType, setAppType] = useState('wordpress');
|
|
||||||
const [sampleWebAppType, setSampleWebAppType] = useState('developer'); // New state for sample web app type
|
|
||||||
const [sourceType, setSourceType] = useState('public');
|
|
||||||
const [repoUrl, setRepoUrl] = useState('');
|
|
||||||
const [deploymentKey, setDeploymentKey] = useState('');
|
|
||||||
const [fileName, setFileName] = useState('');
|
|
||||||
|
|
||||||
// Domain configuration
|
|
||||||
const [useSubdomain, setUseSubdomain] = useState(true);
|
|
||||||
const [useCustomDomain, setUseCustomDomain] = useState(false);
|
|
||||||
const [customDomain, setCustomDomain] = useState('');
|
|
||||||
const [customSubdomain, setCustomSubdomain] = useState('');
|
|
||||||
const [domainType, setDomainType] = useState('domain'); // 'domain' or 'subdomain'
|
|
||||||
|
|
||||||
// Domain validation states
|
|
||||||
const [isValidating, setIsValidating] = useState(false);
|
|
||||||
const [isValidDomain, setIsValidDomain] = useState(false);
|
|
||||||
const [validationMessage, setValidationMessage] = useState('');
|
|
||||||
|
|
||||||
// DNS configuration
|
|
||||||
const [dnsMethod, setDnsMethod] = useState('cname');
|
|
||||||
const [showDnsConfig, setShowDnsConfig] = useState(false);
|
|
||||||
const [dnsVerified, setDnsVerified] = useState({
|
|
||||||
cname: false,
|
|
||||||
ns: false,
|
|
||||||
a: false,
|
|
||||||
ip: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form validation
|
|
||||||
const [formValid, setFormValid] = useState(true);
|
|
||||||
|
|
||||||
// Toast notification
|
|
||||||
const [toast, setToast] = useState({ visible: false, message: '' });
|
|
||||||
|
|
||||||
// File upload reference
|
|
||||||
const fileInputRef = React.useRef(null);
|
|
||||||
|
|
||||||
// Effect for handling domain type changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!useCustomDomain) {
|
|
||||||
setShowDnsConfig(false);
|
|
||||||
setIsValidDomain(false);
|
|
||||||
setValidationMessage('');
|
|
||||||
setIsValidating(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
}, [useCustomDomain, dnsVerified.cname, dnsVerified.ns, dnsVerified.ns, domainType, dnsMethod]);
|
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
const showToast = (message) => {
|
|
||||||
setToast({ visible: true, message });
|
|
||||||
setTimeout(() => setToast({ visible: false, message: '' }), 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle deployment type change
|
|
||||||
const handleDeploymentTypeChange = (e) => {
|
|
||||||
setDeploymentType(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle source type change
|
|
||||||
const handleSourceTypeChange = (e) => {
|
|
||||||
setSourceType(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle file upload
|
|
||||||
const handleFileChange = (e) => {
|
|
||||||
if (e.target.files.length > 0) {
|
|
||||||
setFileName(e.target.files[0].name);
|
|
||||||
} else {
|
|
||||||
setFileName('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle domain checkbox changes
|
|
||||||
const handleUseSubdomainChange = (e) => {
|
|
||||||
setUseSubdomain(e.target.checked);
|
|
||||||
|
|
||||||
// If CNAME record is selected, SiliconPin subdomain must be enabled
|
|
||||||
if (useCustomDomain && dnsMethod === 'cname' && !e.target.checked) {
|
|
||||||
setUseCustomDomain(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUseCustomDomainChange = (e) => {
|
|
||||||
setUseCustomDomain(e.target.checked);
|
|
||||||
if (!e.target.checked) {
|
|
||||||
setShowDnsConfig(false);
|
|
||||||
setIsValidDomain(false);
|
|
||||||
setValidationMessage('');
|
|
||||||
setIsValidating(false);
|
|
||||||
setDnsVerified({
|
|
||||||
cname: false,
|
|
||||||
ns: false,
|
|
||||||
a: false,
|
|
||||||
ip: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Force SiliconPin subdomain to be checked if custom domain is checked
|
|
||||||
setUseSubdomain(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle domain type change
|
|
||||||
const handleDomainTypeChange = (e) => {
|
|
||||||
setDomainType(e.target.value);
|
|
||||||
|
|
||||||
// Reset validation when changing domain type
|
|
||||||
setIsValidDomain(false);
|
|
||||||
setValidationMessage('');
|
|
||||||
setDnsVerified({
|
|
||||||
cname: false,
|
|
||||||
ns: false,
|
|
||||||
a: false
|
|
||||||
});
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle domain and subdomain input changes
|
|
||||||
const handleDomainChange = (e) => {
|
|
||||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?(www\.)?/i, '').replace(/\/+$/, '').trim();
|
|
||||||
setCustomDomain(cleanedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubdomainChange = (e) => {
|
|
||||||
const cleanedValue = e.target.value.replace(/^(https?:\/\/)?/i, '').replace(/\/+$/, '').trim();
|
|
||||||
setCustomSubdomain(cleanedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle DNS method change
|
|
||||||
const handleDnsMethodChange = (e) => {
|
|
||||||
setDnsMethod(e.target.value);
|
|
||||||
|
|
||||||
// If changing to CNAME, ensure SiliconPin subdomain is enabled
|
|
||||||
if (e.target.value === 'cname') {
|
|
||||||
setUseSubdomain(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset DNS verification
|
|
||||||
setDnsVerified(prev => ({
|
|
||||||
...prev,
|
|
||||||
[e.target.value]: false
|
|
||||||
}));
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate domain
|
|
||||||
const validateDomain = () => {
|
|
||||||
const domain = domainType === 'domain' ? customDomain : customSubdomain;
|
|
||||||
|
|
||||||
if (!domain) {
|
|
||||||
setValidationMessage('Please enter a domain name.');
|
|
||||||
setIsValidDomain(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial validation: check format with regex
|
|
||||||
const validFormat = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(domain);
|
|
||||||
if (!validFormat) {
|
|
||||||
setValidationMessage('Domain format is invalid. Please check your entry.');
|
|
||||||
setIsValidDomain(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsValidating(true);
|
|
||||||
setValidationMessage('');
|
|
||||||
setShowDnsConfig(false);
|
|
||||||
|
|
||||||
// Simulate an API call to validate the domain
|
|
||||||
setTimeout(() => {
|
|
||||||
// Simulate a real domain check - in a real app this would be an API call
|
|
||||||
// call /host-api/v1/domains/validate/?domain=domain.com
|
|
||||||
const checkResult = true; // Assume domain is valid for demo
|
|
||||||
|
|
||||||
setIsValidating(false);
|
|
||||||
setIsValidDomain(checkResult);
|
|
||||||
|
|
||||||
if (checkResult) {
|
|
||||||
setValidationMessage('Domain is valid and registered.');
|
|
||||||
setShowDnsConfig(true);
|
|
||||||
} else {
|
|
||||||
setValidationMessage('Domain appears to be unregistered or unavailable.');
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForm();
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check DNS configuration
|
|
||||||
const checkSubDomainCname = () => {
|
|
||||||
const domainToCheck = customDomain || customSubdomain;
|
|
||||||
|
|
||||||
fetch('http://localhost:2058/host-api/v1/check-c-name/', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: `domain=${encodeURIComponent(domainToCheck)}`
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
checkDnsConfig('cname');
|
|
||||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
|
||||||
console.log('CNAME record:', data.cname);
|
|
||||||
// Handle success - update UI
|
|
||||||
} else {
|
|
||||||
console.error('Error:', data.message);
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fetch error:', error);
|
|
||||||
// Show error to user
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const checkDnsConfig = (type) => {
|
|
||||||
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
|
||||||
|
|
||||||
// Simulate DNS check
|
|
||||||
setTimeout(() => {
|
|
||||||
setDnsVerified(prev => ({
|
|
||||||
...prev,
|
|
||||||
[type]: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
showToast(`${type} verified successfully!`);
|
|
||||||
validateForm();
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Copy to clipboard
|
|
||||||
const copyToClipboard = (text) => {
|
|
||||||
navigator.clipboard.writeText(text)
|
|
||||||
.then(() => {
|
|
||||||
showToast('Copied to clipboard!');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
showToast('Failed to copy: ' + err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate form
|
|
||||||
const validateForm = () => {
|
|
||||||
// For custom domain, require DNS verification
|
|
||||||
if (useCustomDomain) {
|
|
||||||
if (dnsMethod === 'cname' && !dnsVerified.cname) {
|
|
||||||
setFormValid(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dnsMethod === 'ns' && !dnsVerified.ns) {
|
|
||||||
setFormValid(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dnsMethod === 'ip' && !dnsVerified.ip) {
|
|
||||||
setFormValid(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormValid(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formValid) {
|
|
||||||
showToast('Please complete DNS verification before deploying.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In a real app, this would submit the form data to the server
|
|
||||||
console.log([{ deploymentType, appType, sampleWebAppType, sourceType, repoUrl, deploymentKey, useSubdomain, useCustomDomain, customDomain, customSubdomain, domainType, dnsMethod}]);
|
|
||||||
|
|
||||||
showToast('Form submitted successfully!');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
{/* Deployment Type Selection */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
|
|
||||||
<select
|
|
||||||
id="deployment-type"
|
|
||||||
value={deploymentType}
|
|
||||||
onChange={handleDeploymentTypeChange}
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
>
|
|
||||||
<option value="app">💻 Deploy an App</option>
|
|
||||||
<option value="source">⚙️ From Source</option>
|
|
||||||
<option value="static">📄 Static Site Upload</option>
|
|
||||||
<option value="sample-web-app">🌐 Sample Web App</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* App Options */}
|
|
||||||
{deploymentType === 'app' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="app-type" className="block text-white font-medium">Select Application</label>
|
|
||||||
<select
|
|
||||||
id="app-type"
|
|
||||||
value={appType}
|
|
||||||
onChange={(e) => setAppType(e.target.value)}
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
>
|
|
||||||
<option value="wordpress">🔌 WordPress</option>
|
|
||||||
<option value="prestashop">🛒 PrestaShop</option>
|
|
||||||
<option value="laravel">🚀 Laravel</option>
|
|
||||||
<option value="cakephp">🍰 CakePHP</option>
|
|
||||||
<option value="symfony">🎯 Symfony</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sample Web App Options */}
|
|
||||||
{deploymentType === 'sample-web-app' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="sample-web-app-type" className="block text-white font-medium">Select Template Type</label>
|
|
||||||
<select
|
|
||||||
id="sample-web-app-type"
|
|
||||||
value={sampleWebAppType}
|
|
||||||
onChange={(e) => setSampleWebAppType(e.target.value)}
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
>
|
|
||||||
<option value="developer">👨💻 Developer Portfolio</option>
|
|
||||||
<option value="designer">🎨 Designer Portfolio</option>
|
|
||||||
<option value="photographer">📸 Photographer Portfolio</option>
|
|
||||||
<option value="documentation">📚 Documentation Site</option>
|
|
||||||
<option value="business">🏢 Single Page Business Site</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div className="mt-4 p-4 bg-neutral-700/30 rounded-md border border-neutral-600">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<div className="mr-3 text-2xl">ℹ️</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm text-neutral-300">
|
|
||||||
Deploy a ready-to-use template that you can customize. We'll set up the basic structure and you can modify it to fit your needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Template Preview */}
|
|
||||||
<TemplatePreview templateType={sampleWebAppType} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Source Options */}
|
|
||||||
{deploymentType === 'source' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="source-type" className="block text-white font-medium">Source Type</label>
|
|
||||||
<select
|
|
||||||
id="source-type"
|
|
||||||
value={sourceType}
|
|
||||||
onChange={handleSourceTypeChange}
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
>
|
|
||||||
<option value="public">🌐 Public Repository</option>
|
|
||||||
<option value="private">🔒 Private Repository</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<label htmlFor="repo-url" className="block text-white font-medium mb-2">Repository URL</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="repo-url"
|
|
||||||
value={repoUrl}
|
|
||||||
onChange={(e) => setRepoUrl(e.target.value)}
|
|
||||||
placeholder="https://github.com/username/repository"
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sourceType === 'private' && (
|
|
||||||
<div className="pt-2">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<label htmlFor="deployment-key" className="block text-white font-medium">Deployment Key</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => showToast('Deployment keys are used for secure access to private repositories')}
|
|
||||||
className="text-neutral-400 hover:text-white focus:outline-none"
|
|
||||||
aria-label="Deployment Key Information"
|
|
||||||
>
|
|
||||||
<span className="text-lg">❓</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
id="deployment-key"
|
|
||||||
value={deploymentKey}
|
|
||||||
onChange={(e) => setDeploymentKey(e.target.value)}
|
|
||||||
placeholder="Paste your SSH private key here"
|
|
||||||
rows="6"
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37] font-mono text-sm"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-sm text-neutral-400">Your private key is used only for deploying and is never stored on our servers.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Static Site Options */}
|
|
||||||
{deploymentType === 'static' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-neutral-700/50 rounded-md text-neutral-300 text-center">
|
|
||||||
Upload the zip file containing your static website
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<label htmlFor="file-upload" className="block text-white font-medium mb-2">Upload File (ZIP/TAR)</label>
|
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
<label
|
|
||||||
htmlFor="file-upload"
|
|
||||||
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
||||||
<span className="text-3xl mb-3 text-neutral-400">📁</span>
|
|
||||||
<p className="mb-2 text-sm text-neutral-400">
|
|
||||||
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-neutral-500">ZIP or TAR files only (max. 100MB)</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="file-upload"
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
accept=".zip,.tar"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{fileName && (
|
|
||||||
<div className="mt-2 text-sm text-neutral-400">
|
|
||||||
Selected file: {fileName}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Domain Configuration */}
|
|
||||||
<div className="pt-4 border-t border-neutral-700">
|
|
||||||
<h3 className="text-lg font-medium text-white mb-4">Destination</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* SiliconPin Subdomain */}
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="use-subdomain"
|
|
||||||
checked={useSubdomain}
|
|
||||||
onChange={handleUseSubdomainChange}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label htmlFor="use-subdomain" className="block text-white font-medium">Use SiliconPin Subdomain</label>
|
|
||||||
<div className="mt-2 flex">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="subdomain"
|
|
||||||
value={defaultSubdomain}
|
|
||||||
className="rounded-l-md py-2 px-3 bg-neutral-600 border-y border-l border-neutral-600 text-neutral-300 focus:outline-none w-1/3 font-mono"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<span className="rounded-r-md py-2 px-3 bg-neutral-800 border border-neutral-700 text-neutral-400 w-2/3">.subdomain.siliconpin.com</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Custom Domain */}
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="use-custom-domain"
|
|
||||||
checked={useCustomDomain}
|
|
||||||
onChange={handleUseCustomDomainChange}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
|
|
||||||
|
|
||||||
{useCustomDomain && (
|
|
||||||
<div className="mt-3 space-y-4">
|
|
||||||
{/* Domain Type Selection */}
|
|
||||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="domain-type-domain"
|
|
||||||
name="domain-type"
|
|
||||||
value="domain"
|
|
||||||
checked={domainType === 'domain'}
|
|
||||||
onChange={handleDomainTypeChange}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<label htmlFor="domain-type-domain" className="text-white">Root Domain</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="domain-type-subdomain"
|
|
||||||
name="domain-type"
|
|
||||||
value="subdomain"
|
|
||||||
checked={domainType === 'subdomain'}
|
|
||||||
onChange={handleDomainTypeChange}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<label htmlFor="domain-type-subdomain" className="text-white">Subdomain</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Domain Input */}
|
|
||||||
{domainType === 'domain' ? (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="custom-domain"
|
|
||||||
value={customDomain}
|
|
||||||
onChange={handleDomainChange}
|
|
||||||
placeholder="yourdomain.com"
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-neutral-400">
|
|
||||||
Enter domain without http://, www, or trailing slashes (example.com). You can configure www or other subdomains later.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="custom-subdomain"
|
|
||||||
value={customSubdomain}
|
|
||||||
onChange={handleSubdomainChange}
|
|
||||||
placeholder="blog.yourdomain.com"
|
|
||||||
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-neutral-400">
|
|
||||||
Enter the full subdomain without http:// or trailing slashes. www and protocol prefixes will be automatically removed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Domain Validation */}
|
|
||||||
<button
|
|
||||||
disabled={!customDomain && !customSubdomain}
|
|
||||||
type="button"
|
|
||||||
onClick={validateDomain}
|
|
||||||
className={`px-4 py-2 ${ !customDomain && !customSubdomain ? 'bg-neutral-600 cursor-not-allowed' : 'bg-[#6d9e37] focus:ring-[#6d9e37] transition-colors'} text-white font-medium rounded-md transition-colors focus:outline-none`}
|
|
||||||
>
|
|
||||||
Validate Domain
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Validation Status */}
|
|
||||||
{useCustomDomain && isValidating && (
|
|
||||||
<div className="p-3 bg-neutral-700/50 rounded-md">
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
||||||
<p className="text-white">Verifying domain registration and availability...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{useCustomDomain && !isValidating && validationMessage && (
|
|
||||||
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
|
|
||||||
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
|
|
||||||
{validationMessage}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* DNS Configuration Options */}
|
|
||||||
{showDnsConfig && (
|
|
||||||
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
|
|
||||||
<h4 className="text-white font-medium">Connect Your Domain</h4>
|
|
||||||
|
|
||||||
{/* CNAME Record Option */}
|
|
||||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
|
||||||
<label for="dns-cname" className="flex items-start cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="dns-cname"
|
|
||||||
name="dns-method"
|
|
||||||
value="cname"
|
|
||||||
checked={dnsMethod === 'cname'}
|
|
||||||
onChange={handleDnsMethodChange}
|
|
||||||
className="mt-1 mr-2"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
|
|
||||||
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
|
|
||||||
|
|
||||||
<div className="mt-3 flex items-center">
|
|
||||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
|
|
||||||
{defaultSubdomain}.subdomain.siliconpin.com
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
|
|
||||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
|
||||||
aria-label="Copy CNAME value"
|
|
||||||
>
|
|
||||||
<span className="text-lg">📋</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => (checkSubDomainCname())}
|
|
||||||
className={`px-3 py-1 text-white text-sm rounded
|
|
||||||
${dnsVerified.cname
|
|
||||||
? 'bg-green-700 hover:bg-green-600'
|
|
||||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
|
||||||
>
|
|
||||||
{dnsVerified.cname ? '✓ CNAME Verified' : 'Check CNAME'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nameserver Option (only for full domains, not subdomains) */}
|
|
||||||
{domainType === 'domain' && (
|
|
||||||
<>
|
|
||||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
|
||||||
<label for="dns-ns" className="flex items-start cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="dns-ns"
|
|
||||||
name="dns-method"
|
|
||||||
value="ns"
|
|
||||||
checked={dnsMethod === 'ns'}
|
|
||||||
onChange={handleDnsMethodChange}
|
|
||||||
className="mt-1 mr-2"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
|
|
||||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyToClipboard('ns1.siliconpin.com')}
|
|
||||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
|
||||||
aria-label="Copy nameserver value"
|
|
||||||
>
|
|
||||||
<span className="text-lg">📋</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyToClipboard('ns2.siliconpin.com')}
|
|
||||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
|
||||||
aria-label="Copy nameserver value"
|
|
||||||
>
|
|
||||||
<span className="text-lg">📋</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 text-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => checkDnsConfig('ns')}
|
|
||||||
className={`px-3 py-1 text-white text-sm rounded
|
|
||||||
${dnsVerified.ns
|
|
||||||
? 'bg-green-700 hover:bg-green-600'
|
|
||||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
|
||||||
>
|
|
||||||
{dnsVerified.ns ? '✓ Nameservers Verified' : 'Check Nameservers'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
|
||||||
<label for="dns-ip" className="flex items-start cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id="dns-ip"
|
|
||||||
name="dns-method"
|
|
||||||
value="ip"
|
|
||||||
checked={dnsMethod === 'ip'}
|
|
||||||
onChange={handleDnsMethodChange}
|
|
||||||
className="mt-1 mr-2"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label htmlFor="dns-ip" className="block text-white font-medium">Use Our IP Address</label>
|
|
||||||
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
|
||||||
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">xxx.xxx.x.xx</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyToClipboard('xxx.xxx.x.xx')}
|
|
||||||
className="ml-2 text-[#6d9e37] hover:text-white"
|
|
||||||
aria-label="Copy nameserver value"
|
|
||||||
>
|
|
||||||
<span className="text-lg">📋</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => checkDnsConfig('ip')}
|
|
||||||
className={`px-3 py-1 text-white text-sm rounded
|
|
||||||
${dnsMethod === 'ip'
|
|
||||||
? 'bg-green-700 hover:bg-green-600'
|
|
||||||
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
|
||||||
>
|
|
||||||
{/* {dnsVerified.ip ? '✓ IP Address Verified' : 'Check IP Address'} */}
|
|
||||||
Proceed to Pay
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Form Submit Button */}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={useCustomDomain && !formValid}
|
|
||||||
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
|
|
||||||
${useCustomDomain && !formValid
|
|
||||||
? 'bg-neutral-600 cursor-not-allowed'
|
|
||||||
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Start the Deployment
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<Toast visible={toast.visible} message={toast.message} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for app deployment options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.appType - Selected app type
|
||||||
|
* @param {Function} props.onAppTypeChange - Handler for app type change
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const AppDeployment = ({ appType, onAppTypeChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="app-type" className="block text-white font-medium">Select Application</label>
|
||||||
|
<select
|
||||||
|
id="app-type"
|
||||||
|
value={appType}
|
||||||
|
onChange={onAppTypeChange}
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<option value="wordpress">🔌 WordPress</option>
|
||||||
|
<option value="prestashop">🛒 PrestaShop</option>
|
||||||
|
<option value="laravel">🚀 Laravel</option>
|
||||||
|
<option value="cakephp">🍰 CakePHP</option>
|
||||||
|
<option value="symfony">🎯 Symfony</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppDeployment;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for source code deployment options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.sourceType - Source type (public/private)
|
||||||
|
* @param {string} props.repoUrl - Repository URL
|
||||||
|
* @param {string} props.deploymentKey - Deployment key for private repos
|
||||||
|
* @param {Function} props.onSourceTypeChange - Handler for source type change
|
||||||
|
* @param {Function} props.onRepoUrlChange - Handler for repo URL change
|
||||||
|
* @param {Function} props.onDeploymentKeyChange - Handler for deployment key change
|
||||||
|
* @param {Function} props.showToast - Function to show toast notifications
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const SourceDeployment = ({
|
||||||
|
sourceType,
|
||||||
|
repoUrl,
|
||||||
|
deploymentKey,
|
||||||
|
onSourceTypeChange,
|
||||||
|
onRepoUrlChange,
|
||||||
|
onDeploymentKeyChange,
|
||||||
|
showToast
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="source-type" className="block text-white font-medium">Source Type</label>
|
||||||
|
<select
|
||||||
|
id="source-type"
|
||||||
|
value={sourceType}
|
||||||
|
onChange={onSourceTypeChange}
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<option value="public">🌐 Public Repository</option>
|
||||||
|
<option value="private">🔒 Private Repository</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<label htmlFor="repo-url" className="block text-white font-medium mb-2">Repository URL</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="repo-url"
|
||||||
|
value={repoUrl}
|
||||||
|
onChange={onRepoUrlChange}
|
||||||
|
placeholder="https://github.com/username/repository"
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sourceType === 'private' && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<label htmlFor="deployment-key" className="block text-white font-medium">Deployment Key</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => showToast('Deployment keys are used for secure access to private repositories')}
|
||||||
|
className="text-neutral-400 hover:text-white focus:outline-none"
|
||||||
|
aria-label="Deployment Key Information"
|
||||||
|
>
|
||||||
|
<span className="text-lg">❓</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="deployment-key"
|
||||||
|
value={deploymentKey}
|
||||||
|
onChange={onDeploymentKeyChange}
|
||||||
|
placeholder="Paste your SSH private key here"
|
||||||
|
rows="6"
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37] font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-neutral-400">Your private key is used only for deploying and is never stored on our servers.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SourceDeployment;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for static site deployment options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.fileName - Selected file name
|
||||||
|
* @param {Function} props.onFileChange - Handler for file change
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const StaticDeployment = ({ fileName, onFileChange }) => {
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="p-4 bg-neutral-700/50 rounded-md text-neutral-300 text-center">
|
||||||
|
Upload the zip file containing your static website
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<label htmlFor="file-upload" className="block text-white font-medium mb-2">Upload File (ZIP/TAR)</label>
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<label
|
||||||
|
htmlFor="file-upload"
|
||||||
|
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer border-neutral-600 hover:border-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<span className="text-3xl mb-3 text-neutral-400">📁</span>
|
||||||
|
<p className="mb-2 text-sm text-neutral-400">
|
||||||
|
<span className="font-semibold">Click to upload</span> or drag and drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-500">ZIP or TAR files only (max. 100MB)</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
onChange={onFileChange}
|
||||||
|
className="hidden"
|
||||||
|
accept=".zip,.tar"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{fileName && (
|
||||||
|
<div className="mt-2 text-sm text-neutral-400">
|
||||||
|
Selected file: {fileName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StaticDeployment;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for sample web app deployment options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.templateType - Selected template type
|
||||||
|
* @param {Function} props.onTemplateTypeChange - Handler for template type change
|
||||||
|
* @param {React.Component} props.TemplatePreview - Template preview component
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const TemplateDeployment = ({ templateType, onTemplateTypeChange, TemplatePreview }) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="sample-web-app-type" className="block text-white font-medium">Select Template Type</label>
|
||||||
|
<select
|
||||||
|
id="sample-web-app-type"
|
||||||
|
value={templateType}
|
||||||
|
onChange={onTemplateTypeChange}
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<option value="developer">👨💻 Developer Portfolio</option>
|
||||||
|
<option value="designer">🎨 Designer Portfolio</option>
|
||||||
|
<option value="photographer">📸 Photographer Portfolio</option>
|
||||||
|
<option value="documentation">📚 Documentation Site</option>
|
||||||
|
<option value="business">🏢 Single Page Business Site</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Information box */}
|
||||||
|
<div className="mt-4 p-4 bg-neutral-700/30 rounded-md border border-neutral-600">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="mr-3 text-2xl">ℹ️</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-neutral-300">
|
||||||
|
Deploy a ready-to-use template that you can customize. We'll set up the basic structure and you can modify it to fit your needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Template Preview */}
|
||||||
|
<TemplatePreview templateType={templateType} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDeployment;
|
||||||
98
src/components/DomainSetupForm/DeploymentOptions/index.jsx
Normal file
98
src/components/DomainSetupForm/DeploymentOptions/index.jsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AppDeployment from './AppDeployment';
|
||||||
|
import SourceDeployment from './SourceDeployment';
|
||||||
|
import StaticDeployment from './StaticDeployment';
|
||||||
|
import TemplateDeployment from './TemplateDeployment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main component for deployment options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {Object} props.deploymentConfig - Deployment configuration state
|
||||||
|
* @param {Object} props.handlers - Event handlers for deployment options
|
||||||
|
* @param {Function} props.showToast - Function to show toast notifications
|
||||||
|
* @param {React.Component} props.TemplatePreview - Template preview component
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const DeploymentOptions = ({
|
||||||
|
deploymentConfig,
|
||||||
|
handlers,
|
||||||
|
showToast,
|
||||||
|
TemplatePreview
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
appType,
|
||||||
|
sampleWebAppType,
|
||||||
|
sourceType,
|
||||||
|
repoUrl,
|
||||||
|
deploymentKey,
|
||||||
|
fileName
|
||||||
|
} = deploymentConfig;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleDeploymentTypeChange,
|
||||||
|
handleAppTypeChange,
|
||||||
|
handleSampleWebAppTypeChange,
|
||||||
|
handleSourceTypeChange,
|
||||||
|
handleRepoUrlChange,
|
||||||
|
handleDeploymentKeyChange,
|
||||||
|
handleFileChange
|
||||||
|
} = handlers;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Deployment Type Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
|
||||||
|
<select
|
||||||
|
id="deployment-type"
|
||||||
|
value={type}
|
||||||
|
onChange={handleDeploymentTypeChange}
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<option value="app">💻 Deploy an App</option>
|
||||||
|
<option value="source">⚙️ From Source</option>
|
||||||
|
<option value="static">📄 Static Site Upload</option>
|
||||||
|
<option value="sample-web-app">🌐 Sample Web App</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Render different components based on deployment type */}
|
||||||
|
{type === 'app' && (
|
||||||
|
<AppDeployment
|
||||||
|
appType={appType}
|
||||||
|
onAppTypeChange={handleAppTypeChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'sample-web-app' && (
|
||||||
|
<TemplateDeployment
|
||||||
|
templateType={sampleWebAppType}
|
||||||
|
onTemplateTypeChange={handleSampleWebAppTypeChange}
|
||||||
|
TemplatePreview={TemplatePreview}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'source' && (
|
||||||
|
<SourceDeployment
|
||||||
|
sourceType={sourceType}
|
||||||
|
repoUrl={repoUrl}
|
||||||
|
deploymentKey={deploymentKey}
|
||||||
|
onSourceTypeChange={handleSourceTypeChange}
|
||||||
|
onRepoUrlChange={handleRepoUrlChange}
|
||||||
|
onDeploymentKeyChange={handleDeploymentKeyChange}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'static' && (
|
||||||
|
<StaticDeployment
|
||||||
|
fileName={fileName}
|
||||||
|
onFileChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeploymentOptions;
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DnsConfiguration from './DnsConfiguration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for custom domain configuration
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {Object} props.domainConfig - Domain configuration state
|
||||||
|
* @param {Object} props.validation - Domain validation state
|
||||||
|
* @param {Object} props.dnsVerified - DNS verification state
|
||||||
|
* @param {Object} props.handlers - Event handlers for domain config
|
||||||
|
* @param {Function} props.checkDnsConfig - Function to check DNS configuration
|
||||||
|
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||||
|
* @param {Function} props.showToast - Function to show toast notifications
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const CustomDomain = ({
|
||||||
|
domainConfig,
|
||||||
|
validation,
|
||||||
|
dnsVerified,
|
||||||
|
handlers,
|
||||||
|
checkDnsConfig,
|
||||||
|
defaultSubdomain,
|
||||||
|
showToast
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
domainType,
|
||||||
|
customDomain,
|
||||||
|
customSubdomain,
|
||||||
|
dnsMethod
|
||||||
|
} = domainConfig;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isValidating,
|
||||||
|
isValidDomain,
|
||||||
|
validationMessage,
|
||||||
|
showDnsConfig
|
||||||
|
} = validation;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleDomainTypeChange,
|
||||||
|
handleDomainChange,
|
||||||
|
handleSubdomainChange,
|
||||||
|
handleDnsMethodChange,
|
||||||
|
validateDomain
|
||||||
|
} = handlers;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 space-y-4">
|
||||||
|
{/* Domain Type Selection */}
|
||||||
|
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="domain-type-domain"
|
||||||
|
name="domain-type"
|
||||||
|
value="domain"
|
||||||
|
checked={domainType === 'domain'}
|
||||||
|
onChange={handleDomainTypeChange}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<label htmlFor="domain-type-domain" className="text-white">Root Domain</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="domain-type-subdomain"
|
||||||
|
name="domain-type"
|
||||||
|
value="subdomain"
|
||||||
|
checked={domainType === 'subdomain'}
|
||||||
|
onChange={handleDomainTypeChange}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<label htmlFor="domain-type-subdomain" className="text-white">Subdomain</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Domain Input */}
|
||||||
|
{domainType === 'domain' ? (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="custom-domain"
|
||||||
|
value={customDomain}
|
||||||
|
onChange={handleDomainChange}
|
||||||
|
placeholder="yourdomain.com"
|
||||||
|
aria-label="Enter root domain"
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-neutral-400">
|
||||||
|
Enter domain without http://, www, or trailing slashes (example.com). You can configure www or other subdomains later.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="custom-subdomain"
|
||||||
|
value={customSubdomain}
|
||||||
|
onChange={handleSubdomainChange}
|
||||||
|
placeholder="blog.yourdomain.com"
|
||||||
|
aria-label="Enter subdomain"
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-neutral-400">
|
||||||
|
Enter the full subdomain without http:// or trailing slashes. www and protocol prefixes will be automatically removed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Domain Validation */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={validateDomain}
|
||||||
|
disabled={isValidating}
|
||||||
|
className="px-4 py-2 bg-neutral-600 text-white font-medium rounded-md hover:bg-neutral-500 transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isValidating ? 'Validating...' : 'Validate Domain'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Validation Status */}
|
||||||
|
{isValidating && (
|
||||||
|
<div className="p-3 bg-neutral-700/50 rounded-md">
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||||
|
<p className="text-white">Verifying domain registration and availability...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isValidating && validationMessage && (
|
||||||
|
<div className={`p-3 rounded-md ${isValidDomain ? 'bg-green-900/20 border border-green-700/50' : 'bg-red-900/20 border border-red-700/50'}`}>
|
||||||
|
<p className={isValidDomain ? 'text-green-400' : 'text-red-400'}>
|
||||||
|
{validationMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* DNS Configuration Options */}
|
||||||
|
{showDnsConfig && (
|
||||||
|
<DnsConfiguration
|
||||||
|
domainType={domainType}
|
||||||
|
dnsMethod={dnsMethod}
|
||||||
|
defaultSubdomain={defaultSubdomain}
|
||||||
|
dnsVerified={dnsVerified}
|
||||||
|
onDnsMethodChange={handleDnsMethodChange}
|
||||||
|
checkDnsConfig={checkDnsConfig}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomDomain;
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { copyToClipboard } from '../../../utils/domainUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for DNS configuration options
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.domainType - Domain type ('domain' or 'subdomain')
|
||||||
|
* @param {string} props.dnsMethod - DNS method ('cname' or 'ns')
|
||||||
|
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||||
|
* @param {Object} props.dnsVerified - DNS verification state
|
||||||
|
* @param {Function} props.onDnsMethodChange - Handler for DNS method change
|
||||||
|
* @param {Function} props.checkDnsConfig - Function to check DNS configuration
|
||||||
|
* @param {Function} props.showToast - Function to show toast notifications
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const DnsConfiguration = ({
|
||||||
|
domainType,
|
||||||
|
dnsMethod,
|
||||||
|
defaultSubdomain,
|
||||||
|
dnsVerified,
|
||||||
|
onDnsMethodChange,
|
||||||
|
checkDnsConfig,
|
||||||
|
showToast
|
||||||
|
}) => {
|
||||||
|
// Handle copy to clipboard with toast feedback
|
||||||
|
const handleCopyToClipboard = async (text) => {
|
||||||
|
const result = await copyToClipboard(text);
|
||||||
|
if (result.success) {
|
||||||
|
showToast('Copied to clipboard!');
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to copy: ${result.error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 space-y-4 p-4 rounded-md bg-neutral-800/50 border border-neutral-700">
|
||||||
|
<h4 className="text-white font-medium">Connect Your Domain</h4>
|
||||||
|
|
||||||
|
{/* CNAME Record Option */}
|
||||||
|
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="dns-cname"
|
||||||
|
name="dns-method"
|
||||||
|
value="cname"
|
||||||
|
checked={dnsMethod === 'cname'}
|
||||||
|
onChange={onDnsMethodChange}
|
||||||
|
className="mt-1 mr-2"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="dns-cname" className="block text-white font-medium">Use CNAME Record</label>
|
||||||
|
<p className="text-sm text-neutral-300">Point your domain to our SiliconPin subdomain</p>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-center">
|
||||||
|
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">
|
||||||
|
{defaultSubdomain}.subdomain.siliconpin.com
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopyToClipboard(`${defaultSubdomain}.subdomain.siliconpin.com`)}
|
||||||
|
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||||
|
aria-label="Copy CNAME value"
|
||||||
|
>
|
||||||
|
<span className="text-lg">📋</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => checkDnsConfig('cname')}
|
||||||
|
disabled={dnsVerified.cname === 'checking'}
|
||||||
|
className={`px-3 py-1 text-white text-sm rounded
|
||||||
|
${dnsVerified.cname === true
|
||||||
|
? 'bg-green-700 hover:bg-green-600'
|
||||||
|
: dnsVerified.cname === 'checking'
|
||||||
|
? 'bg-neutral-500 cursor-not-allowed'
|
||||||
|
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||||
|
>
|
||||||
|
{dnsVerified.cname === true
|
||||||
|
? '✓ CNAME Verified'
|
||||||
|
: dnsVerified.cname === 'checking'
|
||||||
|
? 'Checking...'
|
||||||
|
: 'Check CNAME'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nameserver Option (only for full domains, not subdomains) */}
|
||||||
|
{domainType === 'domain' && (
|
||||||
|
<div className="p-4 bg-neutral-700/30 rounded-md border border-neutral-600 space-y-3">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="dns-ns"
|
||||||
|
name="dns-method"
|
||||||
|
value="ns"
|
||||||
|
checked={dnsMethod === 'ns'}
|
||||||
|
onChange={onDnsMethodChange}
|
||||||
|
className="mt-1 mr-2"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="dns-ns" className="block text-white font-medium">Use Our Nameservers</label>
|
||||||
|
<p className="text-sm text-neutral-300">Update your domain's nameservers to use ours</p>
|
||||||
|
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns1.siliconpin.com</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopyToClipboard('ns1.siliconpin.com')}
|
||||||
|
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||||
|
aria-label="Copy nameserver value"
|
||||||
|
>
|
||||||
|
<span className="text-lg">📋</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-neutral-800 p-2 rounded font-mono text-sm text-neutral-300">ns2.siliconpin.com</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCopyToClipboard('ns2.siliconpin.com')}
|
||||||
|
className="ml-2 text-[#6d9e37] hover:text-white"
|
||||||
|
aria-label="Copy nameserver value"
|
||||||
|
>
|
||||||
|
<span className="text-lg">📋</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => checkDnsConfig('ns')}
|
||||||
|
disabled={dnsVerified.ns === 'checking'}
|
||||||
|
className={`px-3 py-1 text-white text-sm rounded
|
||||||
|
${dnsVerified.ns === true
|
||||||
|
? 'bg-green-700 hover:bg-green-600'
|
||||||
|
: dnsVerified.ns === 'checking'
|
||||||
|
? 'bg-neutral-500 cursor-not-allowed'
|
||||||
|
: 'bg-neutral-600 hover:bg-neutral-500'}`}
|
||||||
|
>
|
||||||
|
{dnsVerified.ns === true
|
||||||
|
? '✓ Nameservers Verified'
|
||||||
|
: dnsVerified.ns === 'checking'
|
||||||
|
? 'Checking...'
|
||||||
|
: 'Check Nameservers'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DnsConfiguration;
|
||||||
91
src/components/DomainSetupForm/DomainConfiguration/index.jsx
Normal file
91
src/components/DomainSetupForm/DomainConfiguration/index.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CustomDomain from './CustomDomain';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main component for domain configuration
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {Object} props.domainConfig - Domain configuration state
|
||||||
|
* @param {Object} props.validation - Domain validation state
|
||||||
|
* @param {Object} props.dnsVerified - DNS verification state
|
||||||
|
* @param {Object} props.handlers - Event handlers for domain config
|
||||||
|
* @param {Function} props.checkDnsConfig - Function to check DNS config
|
||||||
|
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||||
|
* @param {Function} props.showToast - Function to show toast notifications
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
const DomainConfiguration = ({
|
||||||
|
domainConfig,
|
||||||
|
validation,
|
||||||
|
dnsVerified,
|
||||||
|
handlers,
|
||||||
|
checkDnsConfig,
|
||||||
|
defaultSubdomain,
|
||||||
|
showToast
|
||||||
|
}) => {
|
||||||
|
const { useSubdomain, useCustomDomain } = domainConfig;
|
||||||
|
const {
|
||||||
|
handleUseSubdomainChange,
|
||||||
|
handleUseCustomDomainChange
|
||||||
|
} = handlers;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-4 border-t border-neutral-700">
|
||||||
|
<h3 className="text-lg font-medium text-white mb-4">Destination</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* SiliconPin Subdomain */}
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="use-subdomain"
|
||||||
|
checked={useSubdomain}
|
||||||
|
onChange={handleUseSubdomainChange}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="use-subdomain" className="block text-white font-medium">Use SiliconPin Subdomain</label>
|
||||||
|
<div className="mt-2 flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="subdomain"
|
||||||
|
value={defaultSubdomain}
|
||||||
|
className="rounded-l-md py-2 px-3 bg-neutral-600 border-y border-l border-neutral-600 text-neutral-300 focus:outline-none w-1/3 font-mono"
|
||||||
|
readOnly
|
||||||
|
aria-label="Default subdomain"
|
||||||
|
/>
|
||||||
|
<span className="rounded-r-md py-2 px-3 bg-neutral-800 border border-neutral-700 text-neutral-400 w-2/3">.subdomain.siliconpin.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Domain */}
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="use-custom-domain"
|
||||||
|
checked={useCustomDomain}
|
||||||
|
onChange={handleUseCustomDomainChange}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label htmlFor="use-custom-domain" className="block text-white font-medium">Use Custom Domain</label>
|
||||||
|
|
||||||
|
{useCustomDomain && (
|
||||||
|
<CustomDomain
|
||||||
|
domainConfig={domainConfig}
|
||||||
|
validation={validation}
|
||||||
|
dnsVerified={dnsVerified}
|
||||||
|
handlers={handlers}
|
||||||
|
checkDnsConfig={checkDnsConfig}
|
||||||
|
defaultSubdomain={defaultSubdomain}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DomainConfiguration;
|
||||||
136
src/components/DomainSetupForm/index.jsx
Normal file
136
src/components/DomainSetupForm/index.jsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import DeploymentOptions from './DeploymentOptions';
|
||||||
|
import DomainConfiguration from './DomainConfiguration';
|
||||||
|
import { Toast } from '../shared/Toast';
|
||||||
|
import { TemplatePreview } from '../shared/TemplatePreview';
|
||||||
|
|
||||||
|
// Custom hooks
|
||||||
|
import useDeploymentConfig from '../../hooks/useDeploymentConfig';
|
||||||
|
import useDomainConfig from '../../hooks/useDomainConfig';
|
||||||
|
import useDnsVerification from '../../hooks/useDnsVerification';
|
||||||
|
import useToast from '../../hooks/useToast';
|
||||||
|
import useFormValidation from '../../hooks/useFormValidation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main DomainSetupForm component
|
||||||
|
* @param {Object} props - Component props
|
||||||
|
* @param {string} props.defaultSubdomain - Default SiliconPin subdomain
|
||||||
|
* @returns {JSX.Element} - Rendered component
|
||||||
|
*/
|
||||||
|
export const DomainSetupForm = ({ defaultSubdomain }) => {
|
||||||
|
// Initialize hooks for different concerns
|
||||||
|
const { toast, showToast } = useToast();
|
||||||
|
|
||||||
|
// Deployment configuration
|
||||||
|
const deploymentConfig = useDeploymentConfig();
|
||||||
|
|
||||||
|
// DNS verification (depends on domain config for reset logic)
|
||||||
|
const dnsVerificationHook = useDnsVerification(showToast);
|
||||||
|
const { dnsVerified, checkDnsConfig, resetAllDnsVerification } = dnsVerificationHook;
|
||||||
|
|
||||||
|
// Domain configuration (needs DNS reset function)
|
||||||
|
const domainConfig = useDomainConfig({}, resetAllDnsVerification);
|
||||||
|
|
||||||
|
// Pass the domain config to DNS verification hook for dependency tracking
|
||||||
|
// This is done after initialization to avoid circular dependencies
|
||||||
|
dnsVerificationHook.domainConfig = domainConfig.config;
|
||||||
|
|
||||||
|
// Form validation based on domain and DNS state
|
||||||
|
const { formValid } = useFormValidation(
|
||||||
|
domainConfig.config,
|
||||||
|
domainConfig.validation,
|
||||||
|
dnsVerified
|
||||||
|
);
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
|
const handleSubmit = useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (domainConfig.useCustomDomain && !formValid) {
|
||||||
|
showToast('Please complete domain validation and DNS verification before deploying.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real app, this would submit the form data to the server
|
||||||
|
console.log({
|
||||||
|
deploymentType: deploymentConfig.type,
|
||||||
|
appType: deploymentConfig.appType,
|
||||||
|
sampleWebAppType: deploymentConfig.sampleWebAppType,
|
||||||
|
sourceType: deploymentConfig.sourceType,
|
||||||
|
repoUrl: deploymentConfig.repoUrl,
|
||||||
|
deploymentKey: deploymentConfig.deploymentKey,
|
||||||
|
useSubdomain: domainConfig.useSubdomain,
|
||||||
|
useCustomDomain: domainConfig.useCustomDomain,
|
||||||
|
customDomain: domainConfig.customDomain,
|
||||||
|
customSubdomain: domainConfig.customSubdomain,
|
||||||
|
domainType: domainConfig.domainType,
|
||||||
|
dnsMethod: domainConfig.dnsMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast('Form submitted successfully!');
|
||||||
|
}, [
|
||||||
|
formValid,
|
||||||
|
showToast,
|
||||||
|
deploymentConfig,
|
||||||
|
domainConfig
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-neutral-800 rounded-lg p-6 sm:p-8 border border-neutral-700">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Deployment Options Section */}
|
||||||
|
<DeploymentOptions
|
||||||
|
deploymentConfig={deploymentConfig}
|
||||||
|
handlers={{
|
||||||
|
handleDeploymentTypeChange: deploymentConfig.handleDeploymentTypeChange,
|
||||||
|
handleAppTypeChange: deploymentConfig.handleAppTypeChange,
|
||||||
|
handleSampleWebAppTypeChange: deploymentConfig.handleSampleWebAppTypeChange,
|
||||||
|
handleSourceTypeChange: deploymentConfig.handleSourceTypeChange,
|
||||||
|
handleRepoUrlChange: deploymentConfig.handleRepoUrlChange,
|
||||||
|
handleDeploymentKeyChange: deploymentConfig.handleDeploymentKeyChange,
|
||||||
|
handleFileChange: deploymentConfig.handleFileChange
|
||||||
|
}}
|
||||||
|
showToast={showToast}
|
||||||
|
TemplatePreview={TemplatePreview}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Domain Configuration Section */}
|
||||||
|
<DomainConfiguration
|
||||||
|
domainConfig={domainConfig.config}
|
||||||
|
validation={domainConfig.validation}
|
||||||
|
dnsVerified={dnsVerified}
|
||||||
|
handlers={{
|
||||||
|
handleUseSubdomainChange: domainConfig.handleUseSubdomainChange,
|
||||||
|
handleUseCustomDomainChange: domainConfig.handleUseCustomDomainChange,
|
||||||
|
handleDomainTypeChange: domainConfig.handleDomainTypeChange,
|
||||||
|
handleDnsMethodChange: domainConfig.handleDnsMethodChange,
|
||||||
|
handleDomainChange: domainConfig.handleDomainChange,
|
||||||
|
handleSubdomainChange: domainConfig.handleSubdomainChange,
|
||||||
|
validateDomain: domainConfig.validateDomain
|
||||||
|
}}
|
||||||
|
checkDnsConfig={checkDnsConfig}
|
||||||
|
defaultSubdomain={defaultSubdomain}
|
||||||
|
showToast={showToast}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Form Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={domainConfig.useCustomDomain && !formValid}
|
||||||
|
className={`w-full mt-6 px-6 py-3 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-neutral-800
|
||||||
|
${domainConfig.useCustomDomain && !formValid
|
||||||
|
? 'bg-neutral-600 cursor-not-allowed'
|
||||||
|
: 'bg-[#6d9e37] hover:bg-[#598035] focus:ring-[#6d9e37] transition-colors'
|
||||||
|
}`}
|
||||||
|
aria-label="Start deployment"
|
||||||
|
>
|
||||||
|
Start the Deployment
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Toast visible={toast.visible} message={toast.message} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DomainSetupForm;
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
|
||||||
import Table from "./ui/table";
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
|
||||||
import { Input } from "./ui/input";
|
|
||||||
import { Textarea } from "./ui/textarea";
|
|
||||||
import { Label } from "./ui/label";
|
|
||||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "./ui/select";
|
|
||||||
import { CustomTabs } from "./ui/CustomTabs";
|
|
||||||
import {localizeTime} from "../lib/localizeTime";
|
|
||||||
interface Ticket {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
status: 'open' | 'in-progress' | 'resolved' | 'closed'; // Add 'closed' here
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
created_by: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: number;
|
|
||||||
ticket_id: number;
|
|
||||||
user_id: number;
|
|
||||||
message: string;
|
|
||||||
created_at: string;
|
|
||||||
user_type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL = 'http://localhost:2058/host-api/app/v1/ticket/index.php';
|
|
||||||
|
|
||||||
function Ticketing() {
|
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [selectedTicket, setSelectedTicket] = useState<(Ticket & { status: 'open' | 'in-progress' | 'resolved' | 'closed' }) | null>(null);
|
|
||||||
const [activeTab, setActiveTab] = useState<'open' | 'in-progress' | 'resolved' | 'closed'>('open');
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [isMessageDialogOpen, setIsMessageDialogOpen] = useState(false);
|
|
||||||
const [newMessage, setNewMessage] = useState('');
|
|
||||||
const [formData, setFormData] = useState({title: '', description: '', status: 'open' as const, });
|
|
||||||
|
|
||||||
// Fetch tickets
|
|
||||||
useEffect(() => {fetchTickets(); }, [activeTab]);
|
|
||||||
|
|
||||||
// Fetch messages when ticket is selected
|
|
||||||
useEffect(() => {if (selectedTicket) {fetchMessages(selectedTicket.id);}}, [selectedTicket]);
|
|
||||||
|
|
||||||
const fetchTickets = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}?action=get_tickets&status=${activeTab}`);
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setTickets(data.tickets);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching tickets:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMessages = async (ticketId: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}?action=get_messages&ticket_id=${ticketId}`);
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Messages response:', data); // Debug log
|
|
||||||
if (data.success) {
|
|
||||||
setMessages(data.messages || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching messages:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'create_ticket',
|
|
||||||
...formData,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
fetchTickets(); // Refresh the list
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
setFormData({ title: '', description: '', status: 'open' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating ticket:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStatusChange = async (ticketId: number, newStatus: 'open' | 'in-progress' | 'resolved' | 'closed') => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'update_ticket_status',
|
|
||||||
ticket_id: ticketId,
|
|
||||||
status: newStatus,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
fetchTickets(); // Refresh the list
|
|
||||||
if (selectedTicket) {
|
|
||||||
setSelectedTicket({ ...selectedTicket, status: newStatus });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating ticket status:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendMessage = async () => {
|
|
||||||
if (!selectedTicket || !newMessage.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'add_message',
|
|
||||||
ticket_id: selectedTicket.id,
|
|
||||||
message: newMessage,
|
|
||||||
user_type: 'user'
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
fetchMessages(selectedTicket.id); // Refresh messages
|
|
||||||
setNewMessage('');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteTicket = async (ticketId: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'delete_ticket',
|
|
||||||
ticket_id: ticketId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
fetchTickets(); // Refresh the list
|
|
||||||
if (selectedTicket?.id === ticketId) {
|
|
||||||
setIsMessageDialogOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting ticket:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Table configuration
|
|
||||||
const tableHeaders = ['ID', 'Title', 'Description', 'Status', 'Created At', 'Actions'];
|
|
||||||
const tableData = tickets.map(ticket => ({
|
|
||||||
id: ticket.id,
|
|
||||||
title: ticket.title,
|
|
||||||
description: ticket.description,
|
|
||||||
status: ticket.status,
|
|
||||||
created_at: localizeTime(ticket.created_at),
|
|
||||||
actions: (
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button size="sm" onClick={() => {setSelectedTicket(ticket); setIsMessageDialogOpen(true); }}>View/Reply</Button>
|
|
||||||
<Button size="sm" onClick={() => handleDeleteTicket(ticket.id)} className="bg-red-500 hover:bg-red-600">Delete</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
const statusColors = {open: 'bg-green-500', 'in-progress': 'bg-yellow-500', resolved: 'bg-blue-500', closed: 'bg-red-500', };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-4">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-2xl font-bold">Ticket / Report</h1>
|
|
||||||
<Button size="sm" onClick={() => setIsDialogOpen(true)}>Create Ticket</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomTabs
|
|
||||||
tabs={[
|
|
||||||
{ label: 'Open', value: 'open', content: null },
|
|
||||||
{ label: 'In Progress', value: 'in-progress', content: null },
|
|
||||||
{ label: 'Resolved', value: 'resolved', content: null },
|
|
||||||
{ label: 'Closed', value: 'closed', content: null },
|
|
||||||
]}
|
|
||||||
defaultValue={activeTab}
|
|
||||||
onValueChange={(value) => setActiveTab(value as any)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table className="w-full border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr className="bg-neutral-800 text-white">
|
|
||||||
{
|
|
||||||
tableHeaders.map((thead, index) => (
|
|
||||||
<th key={index} className={`p-2 text-center border-[1px] border-[#6d9e37]`}>{thead}</th>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{
|
|
||||||
tableData.length > 0 ? (
|
|
||||||
<tbody className="[&>tr:nth-child(even)]:bg-[#262626]">
|
|
||||||
{
|
|
||||||
tableData.map((data, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.id}</td>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.title}</td>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium">{data.description}</td>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">
|
|
||||||
<span className={`text-white px-2 py-1 rounded ${statusColors[data.status] || ''}`}>{data.status}</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium text-center">{data.created_at}</td>
|
|
||||||
<td className="px-2 border-[1px] border-[#6d9e37] font-medium flex items-center justify-center p-1">{data.actions}</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
) : (
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th colSpan={6} className="p-4 border-[1px] border-[#6d9e37] font-medium">No {activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Ticket Found</th>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{/* <Table headers={tableHeaders} data={tableData} striped hover /> */}
|
|
||||||
|
|
||||||
{/* Create Ticket Dialog */}
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Create New Ticket</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="title">Title</Label>
|
|
||||||
<Input id="title" name="title" value={formData.title} onChange={handleInputChange} required className="w-full bg-white rounded-md outline-none border border-[#6d9e37] p-2" placeholder="Write ticket title" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="description">Description</Label>
|
|
||||||
<textarea id="description" name="description" value={formData.description} onChange={handleInputChange} rows={4} className="w-full rounded-md outline-none border border-[#6d9e37] p-2" required placeholder="Write description here..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end space-x-2">
|
|
||||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
|
||||||
<Button type="submit">Create</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Ticket Messages Dialog */}
|
|
||||||
<Dialog open={isMessageDialogOpen} onOpenChange={setIsMessageDialogOpen}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Ticket #{selectedTicket?.id}: {selectedTicket?.title}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
{selectedTicket && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-gray-50 rounded">
|
|
||||||
<p className="font-semibold text-gray-500">Ticket Details</p>
|
|
||||||
<p className="text-gray-500">{selectedTicket.description}</p>
|
|
||||||
<p className="text-sm text-gray-500 mt-2 ">
|
|
||||||
<strong>Status:</strong> <span className={`font-bold px-1 rounded-md text-white ${statusColors[selectedTicket.status] || ''}`}>{selectedTicket.status}</span> |
|
|
||||||
<strong>Created:</strong> {new Date(selectedTicket.created_at).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<p className="font-semibold mb-2 text-[#6d9e37]">Conversation</p>
|
|
||||||
<div className="space-y-4 max-h-64 overflow-y-auto">
|
|
||||||
{
|
|
||||||
messages.length > 0 ? (
|
|
||||||
messages.map(message => (
|
|
||||||
<div key={message.id} className={`px-3 py-1 border-b rounded flex flex-col text-gray-500 ${message.user_type === 'user' ? 'justify-end items-end border-[#6d9e37]' : 'justify-start items-start border-gray-500'} `}>
|
|
||||||
<p>{message.message}</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1 inline-flex items-center gap-1">
|
|
||||||
<img src="/assets/clock.svg" alt="" />
|
|
||||||
{localizeTime(message.created_at)}</p>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500">No messages yet</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t pt-4">
|
|
||||||
<Label htmlFor="newMessage" className="text-[#6d9e37]">Add Reply</Label>
|
|
||||||
<div className="flex flex-row gap-x-2">
|
|
||||||
<textarea placeholder="Write your message..." id="newMessage" rows={1} value={newMessage} onChange={(e) => setNewMessage(e.target.value)} className="w-full p-2 mb-2 border border-[#6d9e37] outline-none focus:outline-none rounded text-gray-500 resize-none"></textarea>
|
|
||||||
<Button onClick={handleSendMessage} disabled={!newMessage.trim()} className="px-6">
|
|
||||||
<img src="/assets/send.svg" alt="" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="space-x-2">
|
|
||||||
{selectedTicket.status === 'closed' ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="default"
|
|
||||||
onClick={() => handleStatusChange(selectedTicket.id, 'open')}
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Reopen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{selectedTicket.status !== 'in-progress' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleStatusChange(selectedTicket.id, 'in-progress')}
|
|
||||||
>
|
|
||||||
Mark as In Progress
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{selectedTicket.status !== 'resolved' && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleStatusChange(selectedTicket.id, 'resolved')}
|
|
||||||
>
|
|
||||||
Mark as Resolved
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleStatusChange(selectedTicket.id, 'closed')}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Ticketing;
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { Input } from "./ui/input";
|
|
||||||
import { Label } from "./ui/label";
|
|
||||||
import { Textarea } from "./ui/textarea";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
||||||
import { CustomTabs } from "./ui/CustomTabs";
|
|
||||||
import Table from "./ui/table";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
||||||
import { Card, CardHeader, CardContent, CardFooter } from "./ui/card";
|
|
||||||
import { Badge } from "./ui/badge";
|
|
||||||
import { formatDistanceToNow } from "./ui/date-format";
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id?: number;
|
|
||||||
content: string;
|
|
||||||
sender: 'user' | 'admin';
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Ticket {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
messages: Message[];
|
|
||||||
status: 'open' | 'in-progress' | 'resolved' | 'closed';
|
|
||||||
priority: 'low' | 'medium' | 'high';
|
|
||||||
category?: string;
|
|
||||||
assignedTo?: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Ticketing() {
|
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
|
||||||
const [filteredTickets, setFilteredTickets] = useState<Ticket[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
||||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
|
||||||
const [currentTicket, setCurrentTicket] = useState<Partial<Ticket>>({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
status: 'open',
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'general'
|
|
||||||
});
|
|
||||||
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
|
|
||||||
const [newMessage, setNewMessage] = useState('');
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [activeTab, setActiveTab] = useState('open');
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
|
|
||||||
const API_URL = 'http://localhost:2058/host-api/v1/ticketing/index.php';
|
|
||||||
|
|
||||||
const fetchTickets = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await fetch(API_URL);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch tickets');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
// Convert admin_message to messages array for backward compatibility
|
|
||||||
const processedData = data.map((ticket: any) => ({
|
|
||||||
...ticket,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
content: ticket.description,
|
|
||||||
sender: 'user',
|
|
||||||
createdAt: ticket.createdAt
|
|
||||||
},
|
|
||||||
...(ticket.admin_message ? [{
|
|
||||||
content: ticket.admin_message,
|
|
||||||
sender: 'admin',
|
|
||||||
createdAt: ticket.updatedAt
|
|
||||||
}] : [])
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
setTickets(processedData);
|
|
||||||
filterTickets(processedData, activeTab, searchTerm);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterTickets = (ticketsToFilter: Ticket[], status: string, search: string) => {
|
|
||||||
let filtered = ticketsToFilter;
|
|
||||||
|
|
||||||
if (status !== 'all') {
|
|
||||||
filtered = filtered.filter(ticket => ticket.status === status);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
const searchLower = search.toLowerCase();
|
|
||||||
filtered = filtered.filter(ticket =>
|
|
||||||
ticket.title.toLowerCase().includes(searchLower) ||
|
|
||||||
ticket.description.toLowerCase().includes(searchLower) ||
|
|
||||||
ticket.messages.some(msg => msg.content.toLowerCase().includes(searchLower))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredTickets(filtered);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveTicket = async () => {
|
|
||||||
try {
|
|
||||||
const method = isEditing ? 'PUT' : 'POST';
|
|
||||||
const url = isEditing ? `${API_URL}?id=${currentTicket.id}` : API_URL;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: currentTicket.title,
|
|
||||||
description: currentTicket.description,
|
|
||||||
status: currentTicket.status,
|
|
||||||
priority: currentTicket.priority,
|
|
||||||
category: currentTicket.category
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || `Failed to ${isEditing ? 'update' : 'create'} ticket`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchTickets();
|
|
||||||
setIsDialogOpen(false);
|
|
||||||
resetForm();
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addMessage = async (ticketId: number) => {
|
|
||||||
try {
|
|
||||||
if (!newMessage.trim()) return;
|
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}?id=${ticketId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
message: newMessage
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to add message');
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewMessage('');
|
|
||||||
await fetchTickets();
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reopenTicket = async (ticketId: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}?id=${ticketId}&action=reopen`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to reopen ticket');
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchTickets();
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteTicket = async (id: number) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}?id=${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to delete ticket');
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchTickets();
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewTicketDetails = (ticket: Ticket) => {
|
|
||||||
setSelectedTicket(ticket);
|
|
||||||
setIsDetailOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initNewTicket = () => {
|
|
||||||
setCurrentTicket({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
status: 'open',
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'general'
|
|
||||||
});
|
|
||||||
setIsEditing(false);
|
|
||||||
setIsDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initEditTicket = (ticket: Ticket) => {
|
|
||||||
setCurrentTicket({ ...ticket });
|
|
||||||
setIsEditing(true);
|
|
||||||
setIsDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setCurrentTicket({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
status: 'open',
|
|
||||||
priority: 'medium',
|
|
||||||
category: 'general'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setCurrentTicket(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectChange = (name: string, value: string) => {
|
|
||||||
setCurrentTicket(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTickets();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
filterTickets(tickets, activeTab, searchTerm);
|
|
||||||
}, [activeTab, searchTerm, tickets]);
|
|
||||||
|
|
||||||
const renderTicketTable = () => {
|
|
||||||
if (loading) {
|
|
||||||
return <div className="text-center py-8">Loading tickets...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filteredTickets.length === 0) {
|
|
||||||
return <div className="text-center py-8">No tickets found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableHeaders = ['ID', 'Title', 'Category', 'Status', 'Priority', 'Created', 'Actions'];
|
|
||||||
|
|
||||||
const tableData = filteredTickets.map(ticket => ({
|
|
||||||
'ID': ticket.id,
|
|
||||||
'Title': (
|
|
||||||
<button
|
|
||||||
onClick={() => viewTicketDetails(ticket)}
|
|
||||||
className="text-left hover:underline"
|
|
||||||
>
|
|
||||||
{ticket.title}
|
|
||||||
</button>
|
|
||||||
),
|
|
||||||
'Category': ticket.category || 'General',
|
|
||||||
'Status': (
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
ticket.status === 'open' ? 'default' :
|
|
||||||
ticket.status === 'in-progress' ? 'secondary' :
|
|
||||||
ticket.status === 'resolved' ? 'success' : 'outline'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ticket.status}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
'Priority': (
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
ticket.priority === 'low' ? 'success' :
|
|
||||||
ticket.priority === 'medium' ? 'warning' : 'destructive'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ticket.priority}
|
|
||||||
</Badge>
|
|
||||||
),
|
|
||||||
'Created': formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true }),
|
|
||||||
'Actions': (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => viewTicketDetails(ticket)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => initEditTicket(ticket)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
{ticket.status === 'closed' ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => reopenTicket(ticket.id)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
Reopen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => deleteTicket(ticket.id)}
|
|
||||||
className="h-8"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
headers={tableHeaders}
|
|
||||||
data={tableData}
|
|
||||||
className="border rounded-lg shadow-sm"
|
|
||||||
striped
|
|
||||||
hover
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMessages = () => {
|
|
||||||
if (!selectedTicket) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{selectedTicket.messages.map((message, index) => (
|
|
||||||
<div key={index} className={`flex ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}>
|
|
||||||
<div className={`max-w-[80%] rounded-lg p-4 ${message.sender === 'admin' ? 'bg-gray-100' : 'bg-blue-100'}`}>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<Avatar className="h-6 w-6">
|
|
||||||
<AvatarFallback>{message.sender === 'admin' ? 'A' : 'U'}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span className="font-medium">{message.sender === 'admin' ? 'Admin' : 'You'}</span>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{formatDistanceToNow(new Date(message.createdAt), { addSuffix: true })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
label: "Open Tickets",
|
|
||||||
value: "open",
|
|
||||||
content: renderTicketTable()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "In Progress",
|
|
||||||
value: "in-progress",
|
|
||||||
content: renderTicketTable()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Resolved",
|
|
||||||
value: "resolved",
|
|
||||||
content: renderTicketTable()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Closed Tickets",
|
|
||||||
value: "closed",
|
|
||||||
content: renderTicketTable()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "All Tickets",
|
|
||||||
value: "all",
|
|
||||||
content: renderTicketTable()
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-4">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-2xl font-bold">Ticketing System</h1>
|
|
||||||
<Button onClick={initNewTicket}>
|
|
||||||
Create New Ticket
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mb-4 flex justify-between items-center">
|
|
||||||
<div className="w-1/3">
|
|
||||||
<Input
|
|
||||||
placeholder="Search tickets..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomTabs
|
|
||||||
tabs={tabs}
|
|
||||||
defaultValue="open"
|
|
||||||
onValueChange={(value) => setActiveTab(value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Ticket Dialog */}
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{isEditing ? 'Edit Ticket' : 'Create New Ticket'}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{isEditing ? 'Update the ticket details' : 'Fill in the details for the new ticket'}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label htmlFor="title" className="text-right">
|
|
||||||
Title
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
value={currentTicket.title}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="col-span-3"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label htmlFor="description" className="text-right">
|
|
||||||
Description
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value={currentTicket.description}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="col-span-3"
|
|
||||||
rows={5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label htmlFor="category" className="text-right">
|
|
||||||
Category
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={currentTicket.category}
|
|
||||||
onValueChange={(value) => handleSelectChange('category', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="col-span-3">
|
|
||||||
<SelectValue placeholder="Select category" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="general">General</SelectItem>
|
|
||||||
<SelectItem value="billing">Billing</SelectItem>
|
|
||||||
<SelectItem value="technical">Technical</SelectItem>
|
|
||||||
<SelectItem value="support">Support</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label htmlFor="status" className="text-right">
|
|
||||||
Status
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={currentTicket.status}
|
|
||||||
onValueChange={(value) => handleSelectChange('status', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="col-span-3">
|
|
||||||
<SelectValue placeholder="Select status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="open">Open</SelectItem>
|
|
||||||
<SelectItem value="in-progress">In Progress</SelectItem>
|
|
||||||
<SelectItem value="resolved">Resolved</SelectItem>
|
|
||||||
<SelectItem value="closed">Closed</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
|
||||||
<Label htmlFor="priority" className="text-right">
|
|
||||||
Priority
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={currentTicket.priority}
|
|
||||||
onValueChange={(value) => handleSelectChange('priority', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="col-span-3">
|
|
||||||
<SelectValue placeholder="Select priority" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="low">Low</SelectItem>
|
|
||||||
<SelectItem value="medium">Medium</SelectItem>
|
|
||||||
<SelectItem value="high">High</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setIsDialogOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={saveTicket}>
|
|
||||||
{isEditing ? 'Update Ticket' : 'Create Ticket'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Ticket Detail Dialog */}
|
|
||||||
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
|
|
||||||
<DialogContent className="max-w-3xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{selectedTicket?.title}</DialogTitle>
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<Badge variant={
|
|
||||||
selectedTicket?.status === 'open' ? 'default' :
|
|
||||||
selectedTicket?.status === 'in-progress' ? 'secondary' :
|
|
||||||
selectedTicket?.status === 'resolved' ? 'success' : 'outline'
|
|
||||||
}>
|
|
||||||
{selectedTicket?.status}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant={
|
|
||||||
selectedTicket?.priority === 'low' ? 'success' :
|
|
||||||
selectedTicket?.priority === 'medium' ? 'warning' : 'destructive'
|
|
||||||
}>
|
|
||||||
{selectedTicket?.priority}
|
|
||||||
</Badge>
|
|
||||||
<span className="text-sm text-gray-500">
|
|
||||||
Created {formatDistanceToNow(new Date(selectedTicket?.createdAt || ''), { addSuffix: true })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="py-4 max-h-[60vh] overflow-y-auto">
|
|
||||||
{renderMessages()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedTicket?.status !== 'closed' && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
value={newMessage}
|
|
||||||
onChange={(e) => setNewMessage(e.target.value)}
|
|
||||||
placeholder="Type your message..."
|
|
||||||
className="flex-1"
|
|
||||||
/>
|
|
||||||
<Button onClick={() => selectedTicket && addMessage(selectedTicket.id)}>
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
22
src/components/atoms/SelectWithLabel/SelectWithLabel.tsx
Normal file
22
src/components/atoms/SelectWithLabel/SelectWithLabel.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SelectWithLabel = ({value, onChange}: any)=>{
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label htmlFor="deployment-type" className="block text-white font-medium">Deployment Type</label>
|
||||||
|
<select
|
||||||
|
id="deployment-type"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className="w-full rounded-md py-2 px-3 bg-neutral-700 border border-neutral-600 text-white focus:outline-none focus:ring-2 focus:ring-[#6d9e37]"
|
||||||
|
>
|
||||||
|
<option value="app">💻 Deploy an App</option>
|
||||||
|
<option value="source">⚙️ From Source</option>
|
||||||
|
<option value="static">📄 Static Site Upload</option>
|
||||||
|
<option value="sample-web-app">🌐 Sample Web App</option>
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectWithLabel;
|
||||||
0
src/components/atoms/SelectWithLabel/index.tsx
Normal file
0
src/components/atoms/SelectWithLabel/index.tsx
Normal file
80
src/components/shared/TemplatePreview.jsx
Normal file
80
src/components/shared/TemplatePreview.jsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const TemplatePreview = ({ templateType }) => {
|
||||||
|
// Template information for different types
|
||||||
|
const templateInfo = {
|
||||||
|
developer: {
|
||||||
|
name: "Developer Portfolio",
|
||||||
|
description: "A modern, responsive portfolio site with sections for projects, skills, and contact information. Perfect for developers to showcase their work.",
|
||||||
|
features: ["Project showcase", "Skills section", "GitHub integration", "Contact form", "Blog ready"],
|
||||||
|
image: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2072&q=80"
|
||||||
|
},
|
||||||
|
designer: {
|
||||||
|
name: "Designer Portfolio",
|
||||||
|
description: "A visually stunning portfolio for designers with image galleries, case studies, and animations to showcase creative work.",
|
||||||
|
features: ["Visual gallery", "Case studies", "Color scheme customization", "Smooth animations", "Design process showcase"],
|
||||||
|
image: "https://images.unsplash.com/photo-1561070791-2526d30994b5?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2000&q=80"
|
||||||
|
},
|
||||||
|
photographer: {
|
||||||
|
name: "Photographer Portfolio",
|
||||||
|
description: "An elegant portfolio with fullscreen galleries, image zooming, and lightbox features designed for photographers to display their work.",
|
||||||
|
features: ["Fullscreen galleries", "Image zoom", "Lightbox", "Category filtering", "Client proofing"],
|
||||||
|
image: "https://images.unsplash.com/photo-1542038784456-1ea8e935640e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80"
|
||||||
|
},
|
||||||
|
documentation: {
|
||||||
|
name: "Documentation Site",
|
||||||
|
description: "A comprehensive documentation site with search, code snippets, and versioning support for technical documentation.",
|
||||||
|
features: ["Search functionality", "Code snippets with syntax highlighting", "Versioning", "Sidebar navigation", "Mobile-friendly"],
|
||||||
|
image: "https://images.unsplash.com/photo-1456406644174-8ddd4cd52a06?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2068&q=80"
|
||||||
|
},
|
||||||
|
business: {
|
||||||
|
name: "Single Page Business Site",
|
||||||
|
description: "A professional one-page website for businesses with sections for services, testimonials, team members, and contact information.",
|
||||||
|
features: ["Single page layout", "Services section", "Testimonials", "Team profiles", "Contact form with map"],
|
||||||
|
image: "https://images.unsplash.com/photo-1560179707-f14e90ef3623?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2073&q=80"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = templateInfo[templateType] || templateInfo.developer;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 bg-neutral-800 rounded-lg border border-neutral-700 overflow-hidden">
|
||||||
|
{/* Template preview image */}
|
||||||
|
<div className="relative h-48 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={template.image}
|
||||||
|
alt={`${template.name} Preview`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end">
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="text-xl font-semibold text-white">{template.name}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Template details */}
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<p className="text-neutral-300">{template.description}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-medium mb-2">Features:</h4>
|
||||||
|
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{template.features.map((feature, index) => (
|
||||||
|
<li key={index} className="text-neutral-400 flex items-center">
|
||||||
|
<span className="mr-2 text-[#6d9e37]">✓</span>
|
||||||
|
{feature}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<button className="px-4 py-2 bg-[#6d9e37] text-white rounded-md hover:bg-[#598035] transition-colors w-full">
|
||||||
|
Select This Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
src/components/shared/Toast.jsx
Normal file
20
src/components/shared/Toast.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Toast = ({ visible, message, type = 'success' }) => {
|
||||||
|
// Color scheme based on type
|
||||||
|
const bgColor = type === 'error' ? 'bg-red-900' : 'bg-green-900';
|
||||||
|
const icon = type === 'error' ? '✕' : '✓';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed bottom-5 right-5 p-3 ${bgColor} text-white rounded-md shadow-lg transform transition-all duration-300 z-50 ${
|
||||||
|
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg">{icon}</span>
|
||||||
|
<span>{message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Tab {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
content: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CustomTabsProps {
|
|
||||||
tabs: Tab[];
|
|
||||||
defaultValue?: string;
|
|
||||||
onValueChange?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CustomTabs({ tabs, defaultValue, onValueChange }: CustomTabsProps) {
|
|
||||||
const [activeTab, setActiveTab] = React.useState(defaultValue || tabs[0]?.value || '');
|
|
||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
|
||||||
setActiveTab(value);
|
|
||||||
if (onValueChange) {
|
|
||||||
onValueChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex border-b border-[#6d9e37]">
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<button
|
|
||||||
key={tab.value}
|
|
||||||
className={`px-4 py-2 font-medium text-sm focus:outline-none ${activeTab === tab.value ? 'border-x border-t border-[#6d9e37] text-[#6d9e37] rounded-t-md' : 'text-[#c7cccb]'}`}
|
|
||||||
onClick={() => handleTabChange(tab.value)}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="py-4">
|
|
||||||
{tabs.find((tab) => tab.value === activeTab)?.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
import { cn } from "../../lib/utils";
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
success:
|
|
||||||
"border-transparent bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
||||||
warning:
|
|
||||||
"border-transparent bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
||||||
info: "border-transparent bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface BadgeProps
|
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants };
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
// Utility function with proper error handling
|
|
||||||
export function customFormatDistanceToNow(
|
|
||||||
date: Date | string | number,
|
|
||||||
options: { addSuffix?: boolean } = { addSuffix: true }
|
|
||||||
): string {
|
|
||||||
try {
|
|
||||||
const dateObj = typeof date === "string" || typeof date === "number"
|
|
||||||
? new Date(date)
|
|
||||||
: date;
|
|
||||||
|
|
||||||
// Check for invalid date
|
|
||||||
if (isNaN(dateObj.getTime())) {
|
|
||||||
return "Invalid date";
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const diffInSeconds = (now.getTime() - dateObj.getTime()) / 1000;
|
|
||||||
|
|
||||||
// Check for infinite or NaN values
|
|
||||||
if (!isFinite(diffInSeconds)) {
|
|
||||||
return "Invalid date range";
|
|
||||||
}
|
|
||||||
|
|
||||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
|
||||||
|
|
||||||
const absoluteSeconds = Math.abs(diffInSeconds);
|
|
||||||
let value: number;
|
|
||||||
let unit: Intl.RelativeTimeFormatUnit;
|
|
||||||
|
|
||||||
if (absoluteSeconds < 60) {
|
|
||||||
value = Math.round(diffInSeconds);
|
|
||||||
unit = 'second';
|
|
||||||
} else if (absoluteSeconds < 3600) {
|
|
||||||
value = Math.round(diffInSeconds / 60);
|
|
||||||
unit = 'minute';
|
|
||||||
} else if (absoluteSeconds < 86400) {
|
|
||||||
value = Math.round(diffInSeconds / 3600);
|
|
||||||
unit = 'hour';
|
|
||||||
} else if (absoluteSeconds < 2592000) {
|
|
||||||
value = Math.round(diffInSeconds / 86400);
|
|
||||||
unit = 'day';
|
|
||||||
} else if (absoluteSeconds < 31536000) {
|
|
||||||
value = Math.round(diffInSeconds / 2592000);
|
|
||||||
unit = 'month';
|
|
||||||
} else {
|
|
||||||
value = Math.round(diffInSeconds / 31536000);
|
|
||||||
unit = 'year';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure value is finite before formatting
|
|
||||||
if (!isFinite(value)) {
|
|
||||||
return "Invalid date range";
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = rtf.format(value, unit);
|
|
||||||
|
|
||||||
if (!options.addSuffix) {
|
|
||||||
result = result.replace(/^in /, '').replace(/ ago$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error formatting date:", error);
|
|
||||||
return "Invalid date";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// React component interface
|
|
||||||
interface DateFormatProps {
|
|
||||||
date: Date | string | number;
|
|
||||||
className?: string;
|
|
||||||
addSuffix?: boolean;
|
|
||||||
fallback?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// React component implementation
|
|
||||||
const DateFormat: React.FC<DateFormatProps> = ({
|
|
||||||
date,
|
|
||||||
className = "",
|
|
||||||
addSuffix = true,
|
|
||||||
fallback = "Invalid date",
|
|
||||||
}) => {
|
|
||||||
let formattedDate: string;
|
|
||||||
try {
|
|
||||||
formattedDate = customFormatDistanceToNow(date, { addSuffix });
|
|
||||||
} catch (error) {
|
|
||||||
formattedDate = fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<time
|
|
||||||
dateTime={new Date(date).toISOString()}
|
|
||||||
className={className}
|
|
||||||
title={new Date(date).toLocaleString()}
|
|
||||||
>
|
|
||||||
{formattedDate}
|
|
||||||
</time>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export both with distinct names
|
|
||||||
export { DateFormat, customFormatDistanceToNow as formatDistanceToNow };
|
|
||||||
@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-md border border-neutral-600 bg-neutral-800 px-3 py-2 text-sm ring-offset-neutral-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#6d9e37] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -22,4 +22,3 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input };
|
export { Input };
|
||||||
|
|
||||||
|
|||||||
@@ -2,57 +2,33 @@ interface TableProps {
|
|||||||
headers: string[];
|
headers: string[];
|
||||||
data: any[];
|
data: any[];
|
||||||
className?: string;
|
className?: string;
|
||||||
caption?: string;
|
children?: React.ReactNode; // Add children here
|
||||||
striped?: boolean;
|
|
||||||
hover?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Table: React.FC<TableProps> = ({
|
const Table: React.FC<TableProps> = ({ headers, data, className = '', children }) => {
|
||||||
headers,
|
|
||||||
data,
|
|
||||||
className = '',
|
|
||||||
caption,
|
|
||||||
striped = false,
|
|
||||||
hover = false
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div className={`overflow-x-auto ${className}`}>
|
<div className={`overflow-x-auto ${className}`}>
|
||||||
<table className="min-w-full bg-white border border-gray-200 rounded-lg">
|
<table className="min-w-full bg-white border border-gray-300">
|
||||||
{caption && (
|
<thead>
|
||||||
<caption className="text-sm text-gray-500 p-2 text-left">
|
|
||||||
{caption}
|
|
||||||
</caption>
|
|
||||||
)}
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
<tr>
|
||||||
{headers.map((header, index) => (
|
{headers.map((header, index) => (
|
||||||
<th
|
<th key={index} className="px-6 py-3 text-left text-sm font-medium text-gray-600">
|
||||||
key={index}
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody>
|
||||||
{data.map((row, index) => (
|
{data.map((row, index) => (
|
||||||
<tr
|
<tr key={index} className="border-t">
|
||||||
key={index}
|
|
||||||
className={`${striped && index % 2 === 0 ? 'bg-gray-50' : ''} ${
|
|
||||||
hover ? 'hover:bg-gray-100' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{Object.values(row).map((value, idx) => (
|
{Object.values(row).map((value, idx) => (
|
||||||
<td
|
<td key={idx} className="px-6 py-4 text-sm text-gray-800">
|
||||||
key={idx}
|
|
||||||
className="px-6 py-4 text-sm text-gray-800"
|
|
||||||
>
|
|
||||||
{value as React.ReactNode}
|
{value as React.ReactNode}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
{children} {/* This will allow you to pass additional content */}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
|
||||||
import { cn } from "../../lib/utils";
|
|
||||||
|
|
||||||
export 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.map((tab) => (
|
|
||||||
<Tabs.Trigger
|
|
||||||
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>
|
|
||||||
))}
|
|
||||||
</Tabs.List>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<Tabs.Content key={tab.value} value={tab.value} className="p-4 text-neutral-300">
|
|
||||||
{tab.content}
|
|
||||||
</Tabs.Content>
|
|
||||||
))}
|
|
||||||
</Tabs.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { CustomTabs };
|
|
||||||
103
src/hooks/useDeploymentConfig.js
Normal file
103
src/hooks/useDeploymentConfig.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing deployment configuration state
|
||||||
|
* @param {Object} initialConfig - Initial deployment configuration
|
||||||
|
* @returns {Object} - Deployment configuration state and updater functions
|
||||||
|
*/
|
||||||
|
const useDeploymentConfig = (initialConfig = {}) => {
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
type: 'app',
|
||||||
|
appType: 'wordpress',
|
||||||
|
sampleWebAppType: 'developer',
|
||||||
|
sourceType: 'public',
|
||||||
|
repoUrl: '',
|
||||||
|
deploymentKey: '',
|
||||||
|
fileName: '',
|
||||||
|
...initialConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract values for easier access
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
appType,
|
||||||
|
sampleWebAppType,
|
||||||
|
sourceType,
|
||||||
|
repoUrl,
|
||||||
|
deploymentKey,
|
||||||
|
fileName
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
// Update a single field
|
||||||
|
const updateField = useCallback((field, value) => {
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handler for deployment type change
|
||||||
|
const handleDeploymentTypeChange = useCallback((e) => {
|
||||||
|
updateField('type', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for app type change
|
||||||
|
const handleAppTypeChange = useCallback((e) => {
|
||||||
|
updateField('appType', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for sample web app type change
|
||||||
|
const handleSampleWebAppTypeChange = useCallback((e) => {
|
||||||
|
updateField('sampleWebAppType', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for source type change
|
||||||
|
const handleSourceTypeChange = useCallback((e) => {
|
||||||
|
updateField('sourceType', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for repo URL change
|
||||||
|
const handleRepoUrlChange = useCallback((e) => {
|
||||||
|
updateField('repoUrl', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for deployment key change
|
||||||
|
const handleDeploymentKeyChange = useCallback((e) => {
|
||||||
|
updateField('deploymentKey', e.target.value);
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for file change
|
||||||
|
const handleFileChange = useCallback((e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
updateField('fileName', e.target.files[0].name);
|
||||||
|
} else {
|
||||||
|
updateField('fileName', '');
|
||||||
|
}
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State values
|
||||||
|
config,
|
||||||
|
type,
|
||||||
|
appType,
|
||||||
|
sampleWebAppType,
|
||||||
|
sourceType,
|
||||||
|
repoUrl,
|
||||||
|
deploymentKey,
|
||||||
|
fileName,
|
||||||
|
|
||||||
|
// Update functions
|
||||||
|
updateField,
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
handleDeploymentTypeChange,
|
||||||
|
handleAppTypeChange,
|
||||||
|
handleSampleWebAppTypeChange,
|
||||||
|
handleSourceTypeChange,
|
||||||
|
handleRepoUrlChange,
|
||||||
|
handleDeploymentKeyChange,
|
||||||
|
handleFileChange
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDeploymentConfig;
|
||||||
62
src/hooks/useDnsVerification.js
Normal file
62
src/hooks/useDnsVerification.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing DNS verification
|
||||||
|
* @param {Function} showToast - Function to display toast notifications
|
||||||
|
* @returns {Object} - DNS verification state and methods
|
||||||
|
*/
|
||||||
|
const useDnsVerification = (showToast) => {
|
||||||
|
// DNS verification state
|
||||||
|
const [dnsVerified, setDnsVerified] = useState({
|
||||||
|
cname: false,
|
||||||
|
ns: false,
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check DNS configuration
|
||||||
|
const checkDnsConfig = useCallback((type) => {
|
||||||
|
showToast(`Checking ${type}... (This would verify DNS in a real app)`);
|
||||||
|
|
||||||
|
// Set type to 'checking' state
|
||||||
|
setDnsVerified(prev => ({
|
||||||
|
...prev,
|
||||||
|
[type]: 'checking'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Simulate DNS check with a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setDnsVerified(prev => ({
|
||||||
|
...prev,
|
||||||
|
[type]: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
showToast(`${type} verified successfully!`);
|
||||||
|
}, 1500);
|
||||||
|
}, [showToast]);
|
||||||
|
|
||||||
|
// Reset DNS verification for a specific type
|
||||||
|
const resetDnsVerification = useCallback((type) => {
|
||||||
|
setDnsVerified(prev => ({
|
||||||
|
...prev,
|
||||||
|
[type]: false
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset all DNS verification
|
||||||
|
const resetAllDnsVerification = useCallback(() => {
|
||||||
|
setDnsVerified({
|
||||||
|
cname: false,
|
||||||
|
ns: false,
|
||||||
|
a: false
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsVerified,
|
||||||
|
checkDnsConfig,
|
||||||
|
resetDnsVerification,
|
||||||
|
resetAllDnsVerification
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDnsVerification;
|
||||||
267
src/hooks/useDomainConfig.js
Normal file
267
src/hooks/useDomainConfig.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { cleanDomainInput, isValidDomainFormat, validateDomainAvailability } from '../utils/domainUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing domain configuration
|
||||||
|
* @param {Object} initialConfig - Initial domain configuration
|
||||||
|
* @param {Function} resetAllDnsVerification - Function to reset all DNS verification
|
||||||
|
* @returns {Object} - Domain configuration state and methods
|
||||||
|
*/
|
||||||
|
const useDomainConfig = (initialConfig = {}, resetAllDnsVerification) => {
|
||||||
|
// Domain configuration state
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
useSubdomain: true,
|
||||||
|
useCustomDomain: false,
|
||||||
|
customDomain: '',
|
||||||
|
customSubdomain: '',
|
||||||
|
domainType: 'domain',
|
||||||
|
dnsMethod: 'cname',
|
||||||
|
...initialConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
// Domain validation state
|
||||||
|
const [validation, setValidation] = useState({
|
||||||
|
isValidating: false,
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract values for easier access
|
||||||
|
const {
|
||||||
|
useSubdomain,
|
||||||
|
useCustomDomain,
|
||||||
|
customDomain,
|
||||||
|
customSubdomain,
|
||||||
|
domainType,
|
||||||
|
dnsMethod
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isValidating,
|
||||||
|
isValidDomain,
|
||||||
|
validationMessage,
|
||||||
|
showDnsConfig
|
||||||
|
} = validation;
|
||||||
|
|
||||||
|
// Update a single field in domain config
|
||||||
|
const updateField = useCallback((field, value) => {
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Update validation state
|
||||||
|
const updateValidation = useCallback((updates) => {
|
||||||
|
setValidation(prev => ({
|
||||||
|
...prev,
|
||||||
|
...updates
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handler for use subdomain checkbox
|
||||||
|
const handleUseSubdomainChange = useCallback((e) => {
|
||||||
|
const newValue = e.target.checked;
|
||||||
|
|
||||||
|
updateField('useSubdomain', newValue);
|
||||||
|
|
||||||
|
// CRITICAL RULE: If CNAME record is selected, SiliconPin subdomain must be enabled
|
||||||
|
// If user tries to uncheck SiliconPin subdomain while using CNAME, disable custom domain
|
||||||
|
if (useCustomDomain && dnsMethod === 'cname' && !newValue) {
|
||||||
|
updateField('useCustomDomain', false);
|
||||||
|
|
||||||
|
// Since we're disabling custom domain, reset validation
|
||||||
|
updateValidation({
|
||||||
|
showDnsConfig: false,
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
isValidating: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also reset DNS verification
|
||||||
|
if (resetAllDnsVerification) {
|
||||||
|
resetAllDnsVerification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [useCustomDomain, dnsMethod, updateField, updateValidation, resetAllDnsVerification]);
|
||||||
|
|
||||||
|
// Handler for use custom domain checkbox
|
||||||
|
const handleUseCustomDomainChange = useCallback((e) => {
|
||||||
|
const newValue = e.target.checked;
|
||||||
|
|
||||||
|
updateField('useCustomDomain', newValue);
|
||||||
|
|
||||||
|
if (!newValue) {
|
||||||
|
// Reset validation when disabling custom domain
|
||||||
|
updateValidation({
|
||||||
|
showDnsConfig: false,
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
isValidating: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset DNS verification
|
||||||
|
if (resetAllDnsVerification) {
|
||||||
|
resetAllDnsVerification();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// CRITICAL RULE: Force SiliconPin subdomain to be checked if custom domain is checked
|
||||||
|
updateField('useSubdomain', true);
|
||||||
|
}
|
||||||
|
}, [updateField, updateValidation, resetAllDnsVerification]);
|
||||||
|
|
||||||
|
// Handler for domain type change
|
||||||
|
const handleDomainTypeChange = useCallback((e) => {
|
||||||
|
updateField('domainType', e.target.value);
|
||||||
|
|
||||||
|
// Reset validation when changing domain type
|
||||||
|
updateValidation({
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset DNS verification
|
||||||
|
if (resetAllDnsVerification) {
|
||||||
|
resetAllDnsVerification();
|
||||||
|
}
|
||||||
|
}, [updateField, updateValidation, resetAllDnsVerification]);
|
||||||
|
|
||||||
|
// Handler for DNS method change
|
||||||
|
const handleDnsMethodChange = useCallback((e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
|
||||||
|
updateField('dnsMethod', newValue);
|
||||||
|
|
||||||
|
// CRITICAL RULE: If changing to CNAME, ensure SiliconPin subdomain is enabled
|
||||||
|
if (newValue === 'cname') {
|
||||||
|
updateField('useSubdomain', true);
|
||||||
|
}
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
// Handler for domain input change
|
||||||
|
const handleDomainChange = useCallback((e) => {
|
||||||
|
const cleanedValue = cleanDomainInput(e.target.value);
|
||||||
|
updateField('customDomain', cleanedValue);
|
||||||
|
|
||||||
|
// Reset domain validation when input changes
|
||||||
|
if (isValidDomain) {
|
||||||
|
updateValidation({
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [updateField, updateValidation, isValidDomain]);
|
||||||
|
|
||||||
|
// Handler for subdomain input change
|
||||||
|
const handleSubdomainChange = useCallback((e) => {
|
||||||
|
const cleanedValue = cleanDomainInput(e.target.value);
|
||||||
|
updateField('customSubdomain', cleanedValue);
|
||||||
|
|
||||||
|
// Reset domain validation when input changes
|
||||||
|
if (isValidDomain) {
|
||||||
|
updateValidation({
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [updateField, updateValidation, isValidDomain]);
|
||||||
|
|
||||||
|
// Validate domain
|
||||||
|
const validateDomain = useCallback(async () => {
|
||||||
|
const domain = domainType === 'domain' ? customDomain : customSubdomain;
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
updateValidation({
|
||||||
|
validationMessage: 'Please enter a domain name.',
|
||||||
|
isValidDomain: false,
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial validation: check format with regex
|
||||||
|
if (!isValidDomainFormat(domain)) {
|
||||||
|
updateValidation({
|
||||||
|
validationMessage: 'Domain format is invalid. Please check your entry.',
|
||||||
|
isValidDomain: false,
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show validation in progress
|
||||||
|
updateValidation({
|
||||||
|
isValidating: true,
|
||||||
|
validationMessage: '',
|
||||||
|
showDnsConfig: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate with API
|
||||||
|
const result = await validateDomainAvailability(domain, domainType);
|
||||||
|
|
||||||
|
updateValidation({
|
||||||
|
isValidating: false,
|
||||||
|
isValidDomain: result.isValid,
|
||||||
|
validationMessage: result.message,
|
||||||
|
showDnsConfig: result.isValid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset DNS verification when domain is validated
|
||||||
|
if (result.isValid && resetAllDnsVerification) {
|
||||||
|
resetAllDnsVerification();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
domainType,
|
||||||
|
customDomain,
|
||||||
|
customSubdomain,
|
||||||
|
updateValidation,
|
||||||
|
resetAllDnsVerification
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reset validation when disabling custom domain
|
||||||
|
useEffect(() => {
|
||||||
|
if (!useCustomDomain) {
|
||||||
|
updateValidation({
|
||||||
|
showDnsConfig: false,
|
||||||
|
isValidDomain: false,
|
||||||
|
validationMessage: '',
|
||||||
|
isValidating: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [useCustomDomain, updateValidation]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State values
|
||||||
|
config,
|
||||||
|
validation,
|
||||||
|
useSubdomain,
|
||||||
|
useCustomDomain,
|
||||||
|
customDomain,
|
||||||
|
customSubdomain,
|
||||||
|
domainType,
|
||||||
|
dnsMethod,
|
||||||
|
isValidating,
|
||||||
|
isValidDomain,
|
||||||
|
validationMessage,
|
||||||
|
showDnsConfig,
|
||||||
|
|
||||||
|
// Update functions
|
||||||
|
updateField,
|
||||||
|
updateValidation,
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
handleUseSubdomainChange,
|
||||||
|
handleUseCustomDomainChange,
|
||||||
|
handleDomainTypeChange,
|
||||||
|
handleDnsMethodChange,
|
||||||
|
handleDomainChange,
|
||||||
|
handleSubdomainChange,
|
||||||
|
validateDomain
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDomainConfig;
|
||||||
57
src/hooks/useFormValidation.js
Normal file
57
src/hooks/useFormValidation.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for form validation based on critical business rules
|
||||||
|
* @param {Object} domainConfig - Domain configuration state
|
||||||
|
* @param {Object} validation - Domain validation state
|
||||||
|
* @param {Object} dnsVerified - DNS verification state
|
||||||
|
* @returns {Object} - Form validation state and methods
|
||||||
|
*/
|
||||||
|
const useFormValidation = (domainConfig, validation, dnsVerified) => {
|
||||||
|
const [formValid, setFormValid] = useState(true);
|
||||||
|
|
||||||
|
const { useCustomDomain, dnsMethod } = domainConfig;
|
||||||
|
const { isValidDomain } = validation;
|
||||||
|
|
||||||
|
// Validate form
|
||||||
|
const validateForm = useCallback(() => {
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
// For custom domain, require domain validation and DNS verification
|
||||||
|
if (useCustomDomain) {
|
||||||
|
// First requirement: domain must be validated successfully
|
||||||
|
if (!isValidDomain) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
// Second requirement: appropriate DNS verification must pass
|
||||||
|
else if (dnsMethod === 'cname' && dnsVerified.cname !== true) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
else if (dnsMethod === 'ns' && dnsVerified.ns !== true) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormValid(valid);
|
||||||
|
return valid;
|
||||||
|
}, [useCustomDomain, isValidDomain, dnsMethod, dnsVerified.cname, dnsVerified.ns]);
|
||||||
|
|
||||||
|
// Validate form when relevant state changes
|
||||||
|
useEffect(() => {
|
||||||
|
validateForm();
|
||||||
|
}, [
|
||||||
|
useCustomDomain,
|
||||||
|
isValidDomain,
|
||||||
|
dnsMethod,
|
||||||
|
dnsVerified.cname,
|
||||||
|
dnsVerified.ns,
|
||||||
|
validateForm
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
formValid,
|
||||||
|
validateForm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFormValidation;
|
||||||
36
src/hooks/useToast.js
Normal file
36
src/hooks/useToast.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing toast notifications
|
||||||
|
* @param {number} duration - Duration in ms to show the toast (default: 3000)
|
||||||
|
* @returns {Object} - Toast state and methods
|
||||||
|
*/
|
||||||
|
const useToast = (duration = 3000) => {
|
||||||
|
const [toast, setToast] = useState({
|
||||||
|
visible: false,
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show a toast notification
|
||||||
|
const showToast = useCallback((message) => {
|
||||||
|
setToast({ visible: true, message });
|
||||||
|
|
||||||
|
// Auto-hide the toast after the specified duration
|
||||||
|
setTimeout(() => {
|
||||||
|
setToast({ visible: false, message: '' });
|
||||||
|
}, duration);
|
||||||
|
}, [duration]);
|
||||||
|
|
||||||
|
// Hide the toast notification
|
||||||
|
const hideToast = useCallback(() => {
|
||||||
|
setToast({ visible: false, message: '' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toast,
|
||||||
|
showToast,
|
||||||
|
hideToast
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useToast;
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export function localizeTime(timeValue: string, targetTimeZone?: string): string {
|
|
||||||
// 1. Auto-detect user's timezone if none provided
|
|
||||||
if (!targetTimeZone) {
|
|
||||||
targetTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Format the date in the target timezone
|
|
||||||
const date = new Date(timeValue.replace(' ', 'T') + 'Z'); // Ensure UTC
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
||||||
timeZone: targetTimeZone,
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [
|
|
||||||
{ value: month },,
|
|
||||||
{ value: day },,
|
|
||||||
{ value: year },,
|
|
||||||
{ value: hour },,
|
|
||||||
{ value: minute },,
|
|
||||||
{ value: second }
|
|
||||||
] = formatter.formatToParts(date);
|
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage:
|
|
||||||
// const localTime = localizeTime(ticket.created_at); // Auto-detects timezone
|
|
||||||
// console.log(localTime); // "2025-04-03 20:14:50" (in user's local time)
|
|
||||||
|
|
||||||
// const timeInTokyo = localizeTime(ticket.created_at, "Asia/Tokyo");
|
|
||||||
// console.log(timeInTokyo); // "2025-04-04 05:14:50" (Tokyo time)
|
|
||||||
@@ -4,9 +4,3 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type helpers
|
|
||||||
export type MergeElementProps<
|
|
||||||
T extends React.ElementType,
|
|
||||||
P extends object = {}
|
|
||||||
> = Omit<React.ComponentPropsWithRef<T>, keyof P> & P;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import { DomainSetupForm } from '../components/DomainSetupForm';
|
import { DomainSetupForm } from '../components/DomainSetupForm/index';
|
||||||
|
|
||||||
// Page-specific SEO metadata
|
// Page-specific SEO metadata
|
||||||
const pageTitle = "Get Started | SiliconPin";
|
const pageTitle = "Get Started | SiliconPin";
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
import Layout from "../layouts/Layout.astro";
|
|
||||||
import TickitingSystem from "../components/Ticket";
|
|
||||||
---
|
|
||||||
<Layout title="">
|
|
||||||
<TickitingSystem client:load />
|
|
||||||
</Layout>
|
|
||||||
72
src/utils/domainUtils.js
Normal file
72
src/utils/domainUtils.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Clean domain input by removing http://, https://, www., and trailing slashes
|
||||||
|
* @param {string} input - The domain input string to clean
|
||||||
|
* @returns {string} - The cleaned domain string
|
||||||
|
*/
|
||||||
|
export const cleanDomainInput = (input) => {
|
||||||
|
return input
|
||||||
|
.replace(/^(https?:\/\/)?(www\.)?/i, '')
|
||||||
|
.replace(/\/+$/, '')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate domain format using regex
|
||||||
|
* @param {string} domain - The domain to validate
|
||||||
|
* @returns {boolean} - Whether the domain format is valid
|
||||||
|
*/
|
||||||
|
export const isValidDomainFormat = (domain) => {
|
||||||
|
if (!domain) return false;
|
||||||
|
return /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/.test(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate domain availability with API
|
||||||
|
* @param {string} domain - The domain to validate
|
||||||
|
* @param {string} type - The domain type ('domain' or 'subdomain')
|
||||||
|
* @returns {Promise} - Promise resolving to validation result
|
||||||
|
*/
|
||||||
|
export const validateDomainAvailability = async (domain, type) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:2058/host-api/v1/validate-domain/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ domain, type })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Network error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
isValid: data.status === "success",
|
||||||
|
message: data.status === "success"
|
||||||
|
? 'Domain is valid and registered.'
|
||||||
|
: 'Domain appears to be unregistered or unavailable.'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error validating domain:', error);
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
message: `Error checking domain: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard
|
||||||
|
* @param {string} text - Text to copy
|
||||||
|
* @returns {Promise} - Promise resolving to success or error
|
||||||
|
*/
|
||||||
|
export const copyToClipboard = async (text) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
39
yarn.lock
39
yarn.lock
@@ -507,21 +507,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-slot" "1.1.2"
|
"@radix-ui/react-slot" "1.1.2"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@1.1.2":
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz"
|
|
||||||
integrity sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==
|
|
||||||
dependencies:
|
|
||||||
"@radix-ui/primitive" "1.1.1"
|
|
||||||
"@radix-ui/react-collection" "1.1.2"
|
|
||||||
"@radix-ui/react-compose-refs" "1.1.1"
|
|
||||||
"@radix-ui/react-context" "1.1.1"
|
|
||||||
"@radix-ui/react-direction" "1.1.0"
|
|
||||||
"@radix-ui/react-id" "1.1.0"
|
|
||||||
"@radix-ui/react-primitive" "2.0.2"
|
|
||||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
|
||||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-select@^2.1.6":
|
"@radix-ui/react-select@^2.1.6":
|
||||||
version "2.1.6"
|
version "2.1.6"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz"
|
||||||
@@ -556,20 +541,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@radix-ui/react-compose-refs" "1.1.1"
|
"@radix-ui/react-compose-refs" "1.1.1"
|
||||||
|
|
||||||
"@radix-ui/react-tabs@^1.1.3":
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz"
|
|
||||||
integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
|
|
||||||
dependencies:
|
|
||||||
"@radix-ui/primitive" "1.1.1"
|
|
||||||
"@radix-ui/react-context" "1.1.1"
|
|
||||||
"@radix-ui/react-direction" "1.1.0"
|
|
||||||
"@radix-ui/react-id" "1.1.0"
|
|
||||||
"@radix-ui/react-presence" "1.1.2"
|
|
||||||
"@radix-ui/react-primitive" "2.0.2"
|
|
||||||
"@radix-ui/react-roving-focus" "1.1.2"
|
|
||||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
|
||||||
|
|
||||||
"@radix-ui/react-toast@^1.2.6":
|
"@radix-ui/react-toast@^1.2.6":
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz"
|
||||||
@@ -770,11 +741,6 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
|
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
|
||||||
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
|
||||||
|
|
||||||
"@types/date-fns@^2.5.3":
|
|
||||||
version "2.5.3"
|
|
||||||
resolved "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz"
|
|
||||||
integrity sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==
|
|
||||||
|
|
||||||
"@types/debug@^4.0.0":
|
"@types/debug@^4.0.0":
|
||||||
version "4.1.12"
|
version "4.1.12"
|
||||||
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
|
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
|
||||||
@@ -1288,11 +1254,6 @@ data-uri-to-buffer@^4.0.0:
|
|||||||
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz"
|
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz"
|
||||||
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
||||||
|
|
||||||
date-fns@^4.1.0:
|
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz"
|
|
||||||
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
|
|
||||||
|
|
||||||
debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.7, debug@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user