Skip to content
Merged
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
32 changes: 9 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,36 +112,22 @@ docs(api): document rewards claim endpoint
- **Authorization**: Use `require_auth()` on top-level callers; rely on `env.invoker()` to gate cross-contract calls
- **Testing**: Write `#[test]` cases for all contract methods using `Env::default()` and `env.mock_all_auths()`

## Testing
### Testing Strategies & Watch Mode Guide

All changes should include appropriate tests. Run the relevant test suite before submitting a PR.

### Backend Tests
#### Watch Mode & Path Filtering
When working on specific modules, run Jest in watch mode or filter by file path to speed up iteration:

```bash
# Unit tests
cd backend && npm test
# Filter backend tests by path pattern
cd backend && npm test -- --testPathPattern=auth

# Watch mode
npm run test:watch
# Watch mode for a specific test file
cd backend && npm run test:watch -- src/auth/auth.service.spec.ts

# With coverage
npm run test:cov

# E2E tests
npm run test:e2e
```

### Frontend Tests

The frontend uses [Vitest](https://vitest.dev/) with [Testing Library](https://testing-library.com/react) for unit tests.

```bash
# Run the test suite
cd frontend && npm test

# Watch mode (re-runs on file changes)
cd frontend && npm run test:watch
# Filter frontend Vitest tests by filename
cd frontend && npm test -- puzzleReviewService
```

Tests live in `frontend/tests/` and use `.test.js` (or `.test.jsx`) extensions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ export class ModerationService {
reason?: ModerationReason,
notes?: string,
): Promise<ModerationResponse[]> {
if (!moderatorId) {
throw new BadRequestException("Moderator authorization required for bulk review updates")
}
this.logger.log(`Bulk moderating ${reviewIds.length} reviews - Action: ${action}`)

const results: ModerationResponse[] = []
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/PuzzleComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ const PuzzleComponent = ({
variant="outline"
disabled={hintLoading || showHint}
onClick={handleHintToggle}
className="group border-white/10 bg-transparent text-gray-300 transition-all duration-300 hover:border-amber-400/30 hover:bg-amber-500/10 hover:text-amber-300"
className="group border-white/10 bg-transparent text-gray-200 transition-all duration-300 hover:border-amber-400/30 hover:bg-amber-500/10 hover:text-amber-300"
>
<span className="flex items-center gap-2">
{hintLoading ? (
Expand Down
48 changes: 29 additions & 19 deletions frontend/components/ShareButton.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Share2 } from "lucide-react";
import { Share2, Check } from "lucide-react";

// Always share the canonical home/referral URL to prevent
// leaking puzzle IDs or internal routes via window.location.href.
Expand All @@ -11,6 +11,8 @@ const SHARE_URL =
"https://stellarhunts.com";

const ShareButton = () => {
const [copied, setCopied] = useState(false);

const handleShare = async () => {
const shareMessage =
"Join me on StellarHunts - solve puzzles and earn exclusive NFTs! 🎮✨";
Expand All @@ -22,29 +24,37 @@ const ShareButton = () => {
text: shareMessage,
url: SHARE_URL,
});
return;
} catch (err) {
navigator.clipboard.writeText(
`${shareMessage}\n${SHARE_URL}`
);
alert("Share link copied to clipboard!");
// Fallback to clipboard without blocking alert
}
} else {
navigator.clipboard.writeText(`${shareMessage}\n${SHARE_URL}`);
alert("Share link copied to clipboard!");
}

if (navigator.clipboard) {
await navigator.clipboard.writeText(`${shareMessage}\n${SHARE_URL}`);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};

return (
<Button
onClick={handleShare}
variant="outline"
className="w-fit border-white/20 text-white hover:bg-white/10 hover:text-white bg-transparent font-bold py-4 px-8 rounded-lg transform transition-all hover:scale-105"
>
<span className="flex items-center gap-2">
<Share2 size={20} />
Share
</span>
</Button>
<div className="relative inline-block">
<Button
onClick={handleShare}
variant="outline"
className="w-fit border-white/20 text-white hover:bg-white/10 hover:text-white bg-transparent font-bold py-4 px-8 rounded-lg transform transition-all hover:scale-105"
>
<span className="flex items-center gap-2">
{copied ? <Check size={20} className="text-green-400" /> : <Share2 size={20} />}
{copied ? "Copied!" : "Share"}
</span>
</Button>
{copied && (
<span className="absolute -top-10 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-xs px-3 py-1.5 rounded shadow-lg whitespace-nowrap z-50">
Share link copied to clipboard!
</span>
)}
</div>
);
};

Expand Down
Loading