-
Notifications
You must be signed in to change notification settings - Fork 32
Feature: Enable Social Sharing of Earned ROXN Rewards #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
| projectName?: string; | ||
| issueTitle?: string; | ||
| issueNumber?: number; | ||
| transactionHash?: string; | ||
| }; | ||
| } | ||
|
|
||
| export function ShareAchievementModal({ | ||
| isOpen, | ||
| onClose, | ||
| achievementData | ||
| }: ShareAchievementModalProps) { | ||
| const [copied, setCopied] = useState(false); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [S] we should use all the capabilities of typescript
|
||
| 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); | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Currency-specific styling | ||
| const getCurrencyStyle = () => { | ||
| switch (currency) { | ||
| case 'ROXN': | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
|
||
| const style = getCurrencyStyle(); | ||
|
|
||
| return ( | ||
| <Dialog open={isOpen} onOpenChange={onClose}> | ||
| <DialogContent className="sm:max-w-md"> | ||
|
Comment on lines
+92
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
- <Dialog open={isOpen} onOpenChange={onClose}>
+ <Dialog
+ open={isOpen}
+ onOpenChange={(open) => {
+ if (!open) onClose();
+ }}
+ >🤖 Prompt for AI Agents |
||
| <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" /> | ||
| </Button> | ||
|
Comment on lines
+171
to
+186
|
||
| </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 | ||
| }; | ||
| } | ||
| 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
|
||||||
| }; | ||||||
|
|
||||||
| /** | ||||||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden 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 |
||||||
|
|
||||||
| /** | ||||||
| * 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' | ||||||
| ); | ||||||
|
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.', | ||||||
|
||||||
| 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.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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 .