mirror of
https://github.com/Hackdex-App/hackdex-website.git
synced 2026-08-23 00:54:14 -05:00
Require hack report details and collect optional contact email (#75)
Details are now required for every report type. Non-stolen reports can include an optional email so staff can follow up; stolen reports still require email. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -356,8 +356,8 @@ export async function submitHackReport(data: {
|
||||
return { error: "Hack not found" };
|
||||
}
|
||||
|
||||
// Validate email if provided (for stolen reports)
|
||||
if (data.reportType === "stolen" && data.email) {
|
||||
// Validate email if provided
|
||||
if (data.email?.trim()) {
|
||||
const emailLower = data.email.trim().toLowerCase();
|
||||
const { error: emailError } = validateEmail(emailLower);
|
||||
if (emailError) {
|
||||
@@ -366,17 +366,12 @@ export async function submitHackReport(data: {
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (data.reportType === "misleading" && !data.details?.trim()) {
|
||||
return { error: "Details are required for misleading reports" };
|
||||
if (data.reportType === "stolen" && !data.email?.trim()) {
|
||||
return { error: "Email is required for stolen hack reports" };
|
||||
}
|
||||
|
||||
if (data.reportType === "stolen") {
|
||||
if (!data.email?.trim()) {
|
||||
return { error: "Email is required for stolen hack reports" };
|
||||
}
|
||||
if (!data.details?.trim()) {
|
||||
return { error: "Details are required for stolen hack reports" };
|
||||
}
|
||||
if (!data.details?.trim()) {
|
||||
return { error: "Details are required for hack reports" };
|
||||
}
|
||||
|
||||
// Build hack URL
|
||||
@@ -417,14 +412,15 @@ export async function submitHackReport(data: {
|
||||
});
|
||||
}
|
||||
|
||||
if (data.email?.trim()) {
|
||||
fields.push({
|
||||
name: "Contact Email",
|
||||
value: data.email.trim().toLowerCase(),
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.reportType === "stolen") {
|
||||
if (data.email) {
|
||||
fields.push({
|
||||
name: "Contact Email",
|
||||
value: data.email.trim().toLowerCase(),
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
if (data.isImpersonating !== null) {
|
||||
fields.push({
|
||||
name: "Is Uploader Impersonating?",
|
||||
|
||||
@@ -45,22 +45,15 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
const canSubmit = () => {
|
||||
if (!reportType) return false;
|
||||
|
||||
if (reportType === "stolen") {
|
||||
// Stolen requires email and details
|
||||
if (!email.trim() || !details.trim()) return false;
|
||||
if (!details.trim()) return false;
|
||||
|
||||
if (email.trim()) {
|
||||
// Basic email validation (matches server-side validation pattern)
|
||||
const emailRegex = /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/;
|
||||
if (!emailRegex.test(email.trim().toLowerCase())) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (reportType === "misleading") {
|
||||
// Misleading requires details
|
||||
return details.trim().length > 0;
|
||||
}
|
||||
|
||||
// Hateful and Harassment are optional, so can always submit
|
||||
return true;
|
||||
return reportType !== "stolen" || !!email.trim();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -74,7 +67,7 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
slug,
|
||||
reportType,
|
||||
details: details.trim() || null,
|
||||
email: reportType === "stolen" ? email.trim() : null,
|
||||
email: email.trim() || null,
|
||||
isImpersonating: reportType === "stolen" ? isImpersonating : null,
|
||||
});
|
||||
|
||||
@@ -174,9 +167,6 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
|
||||
const renderDetailsPage = () => {
|
||||
const isStolen = reportType === "stolen";
|
||||
const isMisleading = reportType === "misleading";
|
||||
const requiresDetails = isStolen || isMisleading;
|
||||
const isOptional = reportType === "hateful" || reportType === "harassment";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-4">
|
||||
@@ -190,45 +180,46 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
<p className="mt-1 text-sm text-foreground/80">
|
||||
{isStolen
|
||||
? "Please provide your contact information and details about the stolen hack."
|
||||
: isOptional
|
||||
? "Please provide additional details (optional)."
|
||||
: "Please provide additional details."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isStolen && (
|
||||
<>
|
||||
<div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isImpersonating}
|
||||
onChange={(e) => setIsImpersonating(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="text-sm font-semibold">Is the uploader impersonating you?</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-semibold mb-2 block">
|
||||
Contact Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="w-full px-3 py-2 rounded-md border border-[var(--border)] bg-[var(--surface-1)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
|
||||
type="checkbox"
|
||||
checked={isImpersonating}
|
||||
onChange={(e) => setIsImpersonating(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<span className="text-sm font-semibold">Is the uploader impersonating you?</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-semibold mb-2 block">
|
||||
Additional Details {requiresDetails && <span className="text-red-500">*</span>}
|
||||
{isOptional && <span className="text-foreground/60 text-xs ml-1">(optional)</span>}
|
||||
Contact Email
|
||||
{isStolen ? (
|
||||
<span className="text-red-500"> *</span>
|
||||
) : (
|
||||
<span className="text-foreground/60 text-xs ml-1">(optional)</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
required={isStolen}
|
||||
className="w-full px-3 py-2 rounded-md border border-[var(--border)] bg-[var(--surface-1)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-semibold mb-2 block">
|
||||
Additional Details <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={details}
|
||||
@@ -239,6 +230,7 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
: "Please provide additional context..."
|
||||
}
|
||||
rows={6}
|
||||
required
|
||||
className="w-full px-3 py-2 rounded-md border border-[var(--border)] bg-[var(--surface-1)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] resize-none"
|
||||
/>
|
||||
</div>
|
||||
@@ -251,6 +243,14 @@ const ReportModal: React.FC<ReportModalProps> = ({ slug, onClose }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isStolen && (
|
||||
<div className="p-4 rounded-md bg-[var(--surface-2)] border border-[var(--border)]">
|
||||
<p className="text-sm text-foreground/90">
|
||||
<strong>Note:</strong> If we have questions, we will reach out via email for clarification.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-4 rounded-md bg-red-500/10 border border-red-500/20">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
|
||||
Reference in New Issue
Block a user