Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 239 additions & 0 deletions client/src/components/share-achievement-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { useState, useRef } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from './ui/dialog';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { useSocialShare } from '../hooks/use-social-share';
import {
Twitter,
Linkedin,
Copy,
Coins,
Award,
ExternalLink,
Check,
Sparkles
} from 'lucide-react';
import { motion } from 'framer-motion';

interface ShareAchievementModalProps {
isOpen: boolean;
onClose: () => void;
achievementData: {
amount: string;
currency: 'XDC' | 'ROXN' | 'USDC';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can write this hardcoded strings values as enum so they can be extensible at one place and while comparing no need to match exact string by writting hardcode instead of that we can use enum . this will reduce the typo error and there will change at one place rule .

enum currencyEnum  { XDC = "XDC" , ROXN = "ROXN" ,USDC = "USDC" }

interface  ShareAchievementModalProps {
achievementData : {
currency : currencyEnum
}

projectName?: string;
issueTitle?: string;
issueNumber?: number;
transactionHash?: string;
};
}

export function ShareAchievementModal({
isOpen,
onClose,
achievementData
}: ShareAchievementModalProps) {
const [copied, setCopied] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[S] we should use all the capabilities of typescript

const [copied, setCopied] = useState<boolean>(false);

const cardRef = useRef<HTMLDivElement>(null);
const { shareOnTwitter, shareOnLinkedIn, copyToClipboard, viewOnExplorer } = useSocialShare();

const { amount, currency, projectName, issueTitle, issueNumber, transactionHash } = achievementData;

const handleShareTwitter = () => {
shareOnTwitter(achievementData);
};

const handleShareLinkedIn = () => {
shareOnLinkedIn(achievementData);
};

const handleCopy = async () => {
const success = await copyToClipboard(achievementData);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};

const handleViewExplorer = () => {
viewOnExplorer(transactionHash);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Currency-specific styling
const getCurrencyStyle = () => {
switch (currency) {
case 'ROXN':

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use above mentioned Enum value as case compare and its better if we pass currency as parameter , it will be more descriptive as unit ( as , i don't have to know check from where it is coming , once i see function , will know currency is parameter , probably , it is passing from where it called )

return {
gradient: 'from-violet-500 via-purple-500 to-fuchsia-500',
bgGradient: 'from-violet-500/20 via-purple-500/10 to-fuchsia-500/20',
text: 'text-violet-400',
border: 'border-violet-500/30'
};
case 'USDC':
return {
gradient: 'from-blue-500 via-cyan-500 to-teal-500',
bgGradient: 'from-blue-500/20 via-cyan-500/10 to-teal-500/20',
text: 'text-cyan-400',
border: 'border-cyan-500/30'
};
case 'XDC':
default:
return {
gradient: 'from-cyan-500 via-emerald-500 to-green-500',
bgGradient: 'from-cyan-500/20 via-emerald-500/10 to-green-500/20',
text: 'text-emerald-400',
border: 'border-emerald-500/30'
};
}
};
Comment on lines +63 to +88

Copilot AI Dec 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name 'getCurrencyStyle' could be more descriptive. Consider renaming it to 'getCurrencyStyleConfig' or 'getCurrencyTheme' to better convey that it returns a configuration object with multiple style properties rather than a single style value.

Copilot uses AI. Check for mistakes.

const style = getCurrencyStyle();

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
Comment on lines +92 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dialog controlled close handler should respect the open boolean.
Passing onClose directly to onOpenChange will call onClose() even when the dialog is opening (depending on the shadcn Dialog behavior). Safer to close only when open === false.

-    <Dialog open={isOpen} onOpenChange={onClose}>
+    <Dialog
+      open={isOpen}
+      onOpenChange={(open) => {
+        if (!open) onClose();
+      }}
+    >
🤖 Prompt for AI Agents
In client/src/components/share-achievement-modal.tsx around lines 95 to 97, the
Dialog's onOpenChange is wired directly to onClose which can run when the dialog
opens; update the handler to accept the open boolean and only call onClose when
the boolean is false (e.g., onOpenChange={(open) => !open && onClose()}),
ensuring you preserve types and any existing props.

<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-yellow-500" />
Share Your Achievement!
</DialogTitle>
<DialogDescription>
Celebrate your contribution and inspire others to join Roxonn.
</DialogDescription>
</DialogHeader>

{/* Achievement Card Preview */}
<div
ref={cardRef}
className="relative overflow-hidden rounded-xl p-6 bg-gradient-to-br from-background via-background to-muted border"
>
{/* Decorative background elements */}
<div className={`absolute inset-0 bg-gradient-to-br ${style.bgGradient} opacity-50`} />
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-yellow-500/10 to-transparent rounded-full blur-2xl" />
<div className="absolute bottom-0 left-0 w-24 h-24 bg-gradient-to-tr from-violet-500/10 to-transparent rounded-full blur-2xl" />

<div className="relative z-10 space-y-4">
{/* Header with Roxonn branding */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-violet-600 to-cyan-600 flex items-center justify-center">
<Award className="h-4 w-4 text-white" />
</div>
<span className="font-semibold text-sm text-muted-foreground">ROXONN</span>
</div>
<Badge variant="outline" className={`${style.border} ${style.text}`}>
Achievement Unlocked
</Badge>
</div>

{/* Main content */}
<div className="text-center py-4">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
<p className="text-sm text-muted-foreground mb-2">I just earned</p>
<div className={`text-4xl font-bold bg-gradient-to-r ${style.gradient} bg-clip-text text-transparent mb-2`}>
{amount} {currency}
</div>
<p className="text-sm text-muted-foreground">for contributing</p>
</motion.div>
</div>

{/* Project info */}
{(projectName || issueTitle) && (
<div className={`rounded-lg bg-card/50 backdrop-blur-sm border ${style.border} p-3`}>
{projectName && (
<p className="text-sm font-medium truncate">{projectName}</p>
)}
{issueTitle && (
<p className="text-xs text-muted-foreground truncate">
{issueNumber && `#${issueNumber} - `}{issueTitle}
</p>
)}
</div>
)}

{/* Footer */}
<div className="flex items-center justify-between pt-2 border-t border-border/50">
<span className="text-xs text-muted-foreground">app.roxonn.com</span>
<div className="flex items-center gap-1">
<Coins className={`h-3 w-3 ${style.text}`} />
<span className="text-xs text-muted-foreground">Blockchain Verified</span>
</div>
</div>
</div>
</div>

{/* Share Buttons */}
<div className="grid grid-cols-2 gap-3">
<Button
onClick={handleShareTwitter}
variant="outline"
className="gap-2 hover:bg-[#1DA1F2]/10 hover:border-[#1DA1F2]/50 hover:text-[#1DA1F2]"
>
<Twitter className="h-4 w-4" />
Share on X
</Button>
<Button
onClick={handleShareLinkedIn}
variant="outline"
className="gap-2 hover:bg-[#0077B5]/10 hover:border-[#0077B5]/50 hover:text-[#0077B5]"
>
<Linkedin className="h-4 w-4" />
LinkedIn
</Button>
Comment on lines +171 to +186

Copilot AI Dec 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The share buttons lack accessible labels that explain what will happen when clicked. Consider adding aria-label attributes that clarify these buttons will open new windows to share on Twitter/X and LinkedIn respectively, which would help users with screen readers understand the action.

Copilot uses AI. Check for mistakes.
</div>

<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleCopy}
className="gap-2"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? 'Copied!' : 'Copy Text'}
</Button>
{transactionHash && (
<Button
variant="ghost"
size="sm"
onClick={handleViewExplorer}
className="gap-2"
>
<ExternalLink className="h-4 w-4" />
View on Explorer
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

// Export a hook for easy usage
export function useShareAchievement() {
const [isOpen, setIsOpen] = useState(false);
const [achievementData, setAchievementData] = useState<ShareAchievementModalProps['achievementData']>({
amount: '0',
currency: 'ROXN'
});

const openShareModal = (data: ShareAchievementModalProps['achievementData']) => {
setAchievementData(data);
setIsOpen(true);
};

const closeShareModal = () => {
setIsOpen(false);
};

return {
isOpen,
achievementData,
openShareModal,
closeShareModal
};
}
145 changes: 145 additions & 0 deletions client/src/hooks/use-social-share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useToast } from './use-toast';

interface ShareOptions {
amount: string;
currency: 'XDC' | 'ROXN' | 'USDC';
projectName?: string;
issueTitle?: string;
issueNumber?: number;
transactionHash?: string;
customMessage?: string;
}

/**
* Hook for sharing ROXN achievements on social media
* Provides methods for Twitter, LinkedIn, and clipboard sharing
*/
export function useSocialShare() {
const { toast } = useToast();

/**
* Generate a share message based on platform and achievement data
*/
const generateShareMessage = (
options: ShareOptions,
platform: 'twitter' | 'linkedin' | 'generic'
): string => {
if (options.customMessage) {
return options.customMessage;
}

const { amount, currency, projectName, issueNumber } = options;
const projectText = projectName ? ` on ${projectName}` : '';
const issueText = issueNumber ? ` (#${issueNumber})` : '';

const baseMessage = `🎉 I just earned ${amount} ${currency} for contributing${projectText}${issueText}!`;

const hashtags = '#Roxonn #XDC #OpenSource #Web3 #Bounty';
const link = 'https://app.roxonn.com';

if (platform === 'twitter') {
return `${baseMessage}\n\n${hashtags}\n\n🚀 Start earning with @RoxonnPlatform:\n${link}`;
} else if (platform === 'linkedin') {
return `${baseMessage}\n\nRoxonn is revolutionizing open-source contributions with blockchain-powered rewards. Join the future of collaborative development!\n\n${link}\n\n${hashtags}`;
}
return `${baseMessage}\n\n${hashtags}\n\n${link}`;
Comment on lines +37 to +45

Copilot AI Dec 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hashtags, link, and platform-specific text are hardcoded as magic strings within the function. Consider extracting these as constants at the module level or in a configuration object to improve maintainability and make them easier to update in the future.

Copilot uses AI. Check for mistakes.
};

/**
* Share on Twitter/X
*/
const shareOnTwitter = (options: ShareOptions) => {
const text = encodeURIComponent(generateShareMessage(options, 'twitter'));
window.open(
`https://twitter.com/intent/tweet?text=${text}`,
'_blank',
'noopener,noreferrer,width=550,height=420'
);
toast({
title: 'Opening Twitter',
description: 'Share your achievement with the world!',
});
};
Comment on lines +51 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden window.open against reverse-tabnabbing (missing noopener).
window.open(..., '_blank', ...) can allow the new page to access window.opener unless you explicitly disable it.

   const shareOnTwitter = (options: ShareOptions) => {
     const text = encodeURIComponent(generateShareMessage(options, 'twitter'));
-    window.open(
+    const w = window.open(
       `https://twitter.com/intent/tweet?text=${text}`,
       '_blank',
-      'width=550,height=420'
+      'noopener,noreferrer,width=550,height=420'
     );
+    if (w) w.opener = null;
     toast({
       title: 'Opening Twitter',
       description: 'Share your achievement with the world!',
     });
   };

   const shareOnLinkedIn = (options: ShareOptions) => {
     const url = encodeURIComponent('https://app.roxonn.com');
     const summary = encodeURIComponent(generateShareMessage(options, 'linkedin'));
-    window.open(
+    const w = window.open(
       `https://www.linkedin.com/sharing/share-offsite/?url=${url}&summary=${summary}`,
       '_blank',
-      'width=550,height=520'
+      'noopener,noreferrer,width=550,height=520'
     );
+    if (w) w.opener = null;
     toast({
       title: 'Opening LinkedIn',
       description: 'Share your professional achievement!',
     });
   };

Also applies to: 67-79

🤖 Prompt for AI Agents
In client/src/hooks/use-social-share.ts around lines 51-62 (and also apply the
same change to 67-79), window.open is currently called with '_blank' which
allows the opened page to access window.opener; update the call to include
'noopener,noreferrer' in the feature string and after opening set the returned
window's opener to null when non-null. Ensure both the Twitter and the other
social share calls use the same pattern (feature string 'noopener,noreferrer'
and a check to set newWindow.opener = null) to prevent reverse-tabnabbing and
suppress referrer leakage.


/**
* Share on LinkedIn
*/
const shareOnLinkedIn = (options: ShareOptions) => {
const url = encodeURIComponent('https://app.roxonn.com');
window.open(
`https://www.linkedin.com/sharing/share-offsite/?url=${url}`,
'_blank',
'noopener,noreferrer,width=550,height=520'
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
toast({
title: 'Opening LinkedIn',
description: 'Share your professional achievement!',
});
};

/**
* Copy share text to clipboard
*/
const copyToClipboard = async (options: ShareOptions): Promise<boolean> => {
try {
const message = generateShareMessage(options, 'generic');
await navigator.clipboard.writeText(message);
toast({
title: 'Copied to clipboard!',
description: 'Share message copied successfully.',
});
return true;
} catch (error) {
toast({
title: 'Failed to copy',
description: 'Could not copy to clipboard.',

Copilot AI Dec 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'Could not copy to clipboard' is generic and doesn't help users understand why the operation failed. Consider providing more context or guidance, such as checking browser permissions or suggesting alternative methods to share the content.

Suggested change
description: 'Could not copy to clipboard.',
description: 'Could not copy to clipboard. This may be due to browser permissions or unsupported features. Please check your browser settings or try copying the text manually.',

Copilot uses AI. Check for mistakes.
variant: 'destructive',
});
return false;
}
};

/**
* View transaction on XDC explorer
*/
const viewOnExplorer = (transactionHash?: string) => {
if (transactionHash) {
const normalizedHash = transactionHash.startsWith('xdc')
? '0x' + transactionHash.slice(3)
: transactionHash;
const explorerUrl = `https://xdcscan.io/tx/${normalizedHash}`;
window.open(explorerUrl, '_blank', 'noopener,noreferrer');
}
};
Comment on lines +105 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Explorer URL host/format is inconsistent with the dashboard.
Dashboard links use xdcscan.io/tx/... and normalize xdc→0x, while this uses xdcscan.com/tx/... without normalization. Recommend centralizing this (single helper) and using it everywhere.

🤖 Prompt for AI Agents
In client/src/hooks/use-social-share.ts around lines 106 to 111, the explorer
link uses the wrong host and format (https://xdcscan.com/tx/...) and does not
normalize XDC addresses to 0x, causing inconsistency with the dashboard; replace
this logic by calling a centralized helper that builds explorer transaction URLs
(ensure it returns https://xdcscan.io/tx/{txHash}) and make that helper perform
address normalization from xdc... to 0x... (or accept already-normalized
addresses), then update this function to use that helper and replace any other
ad-hoc explorer URL construction sites to use the same helper so all links are
consistent.


/**
* Use native share API if available (mobile friendly)
*/
const nativeShare = async (options: ShareOptions): Promise<boolean> => {
if (!navigator.share) {
return false;
}

try {
await navigator.share({
title: `I earned ${options.amount} ${options.currency} on Roxonn!`,
text: generateShareMessage(options, 'generic'),
url: 'https://app.roxonn.com',
});
return true;
} catch (error) {
// User cancelled or share failed
console.debug('Native share failed:', error);
return false;
}
};

return {
shareOnTwitter,
shareOnLinkedIn,
copyToClipboard,
viewOnExplorer,
nativeShare,
generateShareMessage,
};
}
Loading
Loading