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
22 changes: 22 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

# Pre-commit hook: ensure commit author matches the expected GitHub account.

EXPECTED_NAME="FairBid"
EXPECTED_EMAIL="fairbid01@gmail.com"

author_name=$(git config user.name)
author_email=$(git config user.email)

if [ "$author_name" != "$EXPECTED_NAME" ] || [ "$author_email" != "$EXPECTED_EMAIL" ]; then
echo ""
echo "ERROR: Commit author does not match the expected GitHub account."
echo " Current: $author_name <$author_email>"
echo " Expected: $EXPECTED_NAME <$EXPECTED_EMAIL>"
echo ""
echo " To fix, run:"
echo " git config user.name \"$EXPECTED_NAME\""
echo " git config user.email \"$EXPECTED_EMAIL\""
echo ""
exit 1
fi
84 changes: 84 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/bin/sh

# Pre-push hook: prevent pushing orphan branches with disconnected history
# and verify every commit is authored by the expected GitHub account.

EXPECTED_NAME="FairBid"
EXPECTED_EMAIL="fairbid01@gmail.com"

remote="$1"
url="$2"

zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')

while read local_ref local_oid remote_ref remote_oid; do
if [ "$local_oid" = "$zero" ]; then
continue
fi

# ── Verify commit authorship ──────────────────────────────────────────
if [ "$remote_oid" = "$zero" ]; then
range="$local_oid"
else
range="$remote_oid..$local_oid"
fi

bad_commits=$(git log --format="%h %an <%ae>" "$range" 2>/dev/null | \
while read hash name email; do
if [ "$name $email" != "$EXPECTED_NAME <$EXPECTED_EMAIL>" ]; then
echo "$hash $name $email"
fi
done)

if [ -n "$bad_commits" ]; then
echo ""
echo "ERROR: Some commits in this push are not authored by the expected account."
echo " Expected: $EXPECTED_NAME <$EXPECTED_EMAIL>"
echo ""
echo " Offending commits:"
echo "$bad_commits" | while read hash name email; do
echo " $hash $name <$email>"
done
echo ""
echo " To fix, amend each commit to the correct author:"
echo " git commit --amend --author=\"$EXPECTED_NAME <$EXPECTED_EMAIL>\" --no-edit"
echo " # or rebase and edit:"
echo " git rebase -i origin/main -x \"git commit --amend --author='$EXPECTED_NAME <$EXPECTED_EMAIL>' --no-edit\""
echo ""
exit 1
fi

# ── Verify shared history ─────────────────────────────────────────────
if [ "$remote_oid" = "$zero" ]; then
if ! git merge-base --is-ancestor origin/main "$local_oid" 2>/dev/null && \
! git merge-base --is-ancestor origin/master "$local_oid" 2>/dev/null; then
echo ""
echo "ERROR: Cannot push new branch '$local_ref'."
echo " This branch has no shared history with origin/main or origin/master."
echo ""
echo " To fix:"
echo " git fetch origin main"
echo " git checkout -b $local_ref origin/main"
echo " # then cherry-pick or re-apply your changes"
echo ""
exit 1
fi
else
if ! git merge-base --is-ancestor "$remote_oid" "$local_oid"; then
echo ""
echo "ERROR: Cannot push to remote branch '$remote_ref'."
echo " Your local branch and the remote branch have diverged with no shared history."
echo ""
echo " To fix, rebase your work on the remote branch:"
echo " git fetch origin $remote_ref"
echo " git rebase --onto origin/$remote_ref HEAD~ \$(git rev-parse --abbrev-ref HEAD)"
echo " # or reset and cherry-pick:"
echo " git checkout -b temp-branch origin/$remote_ref"
echo " git cherry-pick <your-commit-hash>"
echo ""
exit 1
fi
fi
done

exit 0
70 changes: 70 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Git workflow for this project

## Never use `git init`

This repository already exists on GitHub. Always clone it instead:

```bash
git clone https://github.com/fairbid01/petChain-Frontend.git
cd petChain-Frontend
```

## Creating a new feature branch

Always branch from the remote's latest main:

```bash
git fetch origin main
git checkout -b feature/your-branch-name origin/main
# ... make changes, commit ...
git push -u origin feature/your-branch-name
```

## If you accidentally started with `git init`

Do NOT force-push. Recover by rebasing onto the remote:

```bash
git fetch origin main
git checkout -b temp-rebased origin/main
# Copy your changes from the orphan branch:
git cherry-pick <your-commit-hash>
# Or manually copy changed files:
git checkout <orphan-branch> -- path/to/file1 path/to/file2
git commit -m "your message"
# Replace the broken branch:
git branch -D feature/your-branch
git branch -m feature/your-branch
git push -f -u origin feature/your-branch
```

## After cloning (one-time setup)

Run this to enable the shared pre-push hook:

```bash
git config core.hooksPath .githooks
```

This ensures the hook in `.githooks/pre-push` is used by everyone who clones the repo.

## Commit authorship

All commits must be authored by the GitHub account that owns the token. Before making any commits, set the correct author:

```bash
git config user.name "FairBid"
git config user.email "fairbid01@gmail.com"
```

The pre-commit hook will block commits that don't match the expected author. The pre-push hook will also verify every commit in the push matches the expected author.

## Before pushing

The pre-push hook will reject pushes where your branch has no shared history with the remote. This prevents the "entirely different commit histories" error when opening a PR. If the hook blocks you, follow its instructions to rebase properly.

## Key rules

- **Never** force-push a branch that exists on remote unless you are replacing a broken history
- **Always** `git fetch origin` first to get the latest remote refs
- **Always** base new branches on `origin/main`, never on a local orphan commit
13 changes: 13 additions & 0 deletions src/components/Navigation/NavIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,19 @@ const icons: Record<string, ReactElement> = {
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
),
'trending-up': (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</svg>
),
};

export default function NavIcon({
Expand Down
2 changes: 2 additions & 0 deletions src/components/Navigation/navConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const SIDEBAR_NAV_ITEMS: NavItem[] = [
],
},
{ label: 'Analytics', href: '/analytics', icon: 'chart', authRequired: true },
{ label: 'Exchange Rates', href: '/rate', icon: 'trending-up', authRequired: true },
{ label: 'Rate Us', href: '/review', icon: 'star', authRequired: true },
{ label: 'Notifications', href: '/notifications', icon: 'bell', authRequired: true },
{ label: 'Search', href: '/search', icon: 'search' },
{
Expand Down
141 changes: 141 additions & 0 deletions src/components/Rate/RateHistoryChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React, { useMemo } from 'react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
TooltipProps,
} from 'recharts';
import { HistoricalRatePoint } from '@/lib/api/rateAPI';

interface RateHistoryChartProps {
data: HistoricalRatePoint[];
symbol: string;
interval: string;
}

interface ChartPoint {
label: string;
price: number;
timestamp: string;
}

function formatLabel(timestamp: string, interval: string): string {
const date = new Date(timestamp);
if (interval === '1') {
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
}
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

function formatPrice(value: number): string {
if (value >= 1000) return `$${value.toLocaleString('en-US', { maximumFractionDigits: 2 })}`;
if (value >= 1) return `$${value.toFixed(4)}`;
return `$${value.toFixed(6)}`;
}

function CustomTooltip({ active, payload, label }: TooltipProps<number, string>) {
if (!active || !payload || payload.length === 0) return null;
const value = payload[0]?.value;
return (
<div className="bg-white rounded-xl shadow-lg border border-gray-100 px-3 py-2 text-xs">
<p className="text-gray-500 mb-1">{label}</p>
<p className="font-bold text-blue-700">{value !== undefined ? formatPrice(value) : 'β€”'}</p>
</div>
);
}

export default function RateHistoryChart({ data, symbol, interval }: RateHistoryChartProps) {
const chartData: ChartPoint[] = useMemo(() => {
if (!data || data.length === 0) return [];

// Downsample to at most 60 points for readability
const step = Math.max(1, Math.floor(data.length / 60));
return data
.filter((_, i) => i % step === 0 || i === data.length - 1)
.map((point) => ({
label: formatLabel(point.timestamp, interval),
price: point.priceUSD,
timestamp: point.timestamp,
}));
}, [data, interval]);

if (chartData.length === 0) {
return (
<div className="h-64 flex flex-col items-center justify-center text-gray-400 gap-2">
<svg
className="w-10 h-10 text-gray-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"
/>
</svg>
<p className="text-sm">No historical data available</p>
</div>
);
}

const prices = chartData.map((d) => d.price);
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
const padding = (maxPrice - minPrice) * 0.05 || maxPrice * 0.01;

// Determine trend colour
const isPositive = chartData[chartData.length - 1].price >= chartData[0].price;
const strokeColor = isPositive ? '#10b981' : '#ef4444';
const gradientId = `rateGradient-${symbol}`;

return (
<div className="h-64" role="img" aria-label={`${symbol} price history chart`}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={strokeColor} stopOpacity={0.2} />
<stop offset="95%" stopColor={strokeColor} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" vertical={false} />
<XAxis
dataKey="label"
stroke="#9ca3af"
tick={{ fontSize: 10 }}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
/>
<YAxis
stroke="#9ca3af"
tick={{ fontSize: 10 }}
tickLine={false}
axisLine={false}
tickFormatter={formatPrice}
domain={[minPrice - padding, maxPrice + padding]}
width={70}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="price"
name={`${symbol} Price (USD)`}
stroke={strokeColor}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}
Loading
Loading