Add Playwright configuration and initial tests for landing page
Copilot Setup Steps / copilot-setup-steps (push) Failing after 17s
Copilot Setup Steps / copilot-setup-steps (push) Failing after 17s
- Created playwright.config.ts for test configuration - Added .last-run.json to store test run status - Implemented landing.test.ts with tests for navbar visibility and navigation - Removed unused server proxy configuration from vite.config.ts
This commit is contained in:
@@ -12,42 +12,45 @@ export default function AlpacaAccountInfo() {
|
||||
const fetchAccount = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/alpaca/account");
|
||||
if (!res.ok) throw new Error("API error");
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || "API error");
|
||||
}
|
||||
setAccount(data);
|
||||
} catch {
|
||||
setError("Failed to load account info.");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to load account info.";
|
||||
setError(message);
|
||||
}
|
||||
};
|
||||
fetchAccount();
|
||||
}, []);
|
||||
|
||||
if (error) return <p className="text-red-600">{error}</p>;
|
||||
if (!account) return <p className="text-gray-500">Loading account…</p>;
|
||||
if (error) return <p className="text-red-600 p-4 text-center">{error}</p>;
|
||||
if (!account) return <p className="text-gray-600 p-4 text-center">Loading account…</p>;
|
||||
|
||||
return (
|
||||
<div className="bg-white border rounded-lg p-4 shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-2">Alpaca Account</h2>
|
||||
<dl className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Cash</dt>
|
||||
<dd className="font-mono">
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4 text-center">Trading Account</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-gray-600 font-medium">Cash</span>
|
||||
<span className="text-lg font-bold text-green-600">
|
||||
${account.cash.toLocaleString(undefined, { minimumFractionDigits: 2 })}
|
||||
</dd>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Buying Power</dt>
|
||||
<dd className="font-mono">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span className="text-gray-600 font-medium">Buying Power</span>
|
||||
<span className="text-lg font-bold text-blue-600">
|
||||
${account.buying_power.toLocaleString(undefined, { minimumFractionDigits: 2 })}
|
||||
</dd>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600">Portfolio Value</dt>
|
||||
<dd className="font-mono">
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-gray-600 font-medium">Portfolio Value</span>
|
||||
<span className="text-lg font-bold text-purple-600">
|
||||
${account.portfolio_value.toLocaleString(undefined, { minimumFractionDigits: 2 })}
|
||||
</dd>
|
||||
</span>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Link } from "react-router";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav className="border-b border-gray-200 bg-white/90 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
<Link to="/" className="flex items-center gap-3 group">
|
||||
<div className="h-8 w-8 rounded-lg bg-blue-600 flex items-center justify-center transition-transform group-hover:scale-105">
|
||||
<span className="text-white font-bold text-sm">A</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
AITrader
|
||||
</span>
|
||||
</Link>
|
||||
<div className="hidden items-center gap-8 md:flex">
|
||||
<Link
|
||||
to="/stocks"
|
||||
className="text-gray-600 hover:text-blue-600 font-medium transition-colors relative after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-0.5 after:bg-blue-600 after:transition-all hover:after:w-full"
|
||||
>
|
||||
Stocks
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -19,58 +19,56 @@ export default function StockViewer() {
|
||||
const res = await fetch(
|
||||
`/api/indicators?symbol=${encodeURIComponent(symbol.trim())}`
|
||||
);
|
||||
if (!res.ok) throw new Error("API error");
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "API error");
|
||||
setIndicators(data.indicators);
|
||||
} catch {
|
||||
setError("Failed to fetch indicators. Check the symbol and try again.");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to fetch indicators.";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 max-w-2xl">
|
||||
<h1 className="text-2xl font-bold mb-4">Stock Indicators</h1>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200 max-w-lg mx-auto">
|
||||
<div className="flex gap-3 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={symbol}
|
||||
onChange={(e) => setSymbol(e.target.value)}
|
||||
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
|
||||
placeholder="Enter stock symbol (e.g. AAPL)"
|
||||
className="flex-1 border rounded p-2"
|
||||
className="flex-1 border border-gray-300 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
onKeyDown={(e) => e.key === "Enter" && fetchIndicators()}
|
||||
/>
|
||||
<button
|
||||
onClick={fetchIndicators}
|
||||
disabled={loading || !symbol.trim()}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? "Loading…" : "Get Indicators"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-red-600 mb-4">{error}</p>}
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{indicators && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold mb-2">
|
||||
<h3 className="font-bold text-gray-900 mb-3">
|
||||
Results for {symbol.toUpperCase()}
|
||||
</h2>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left p-2 font-medium">Indicator</th>
|
||||
<th className="text-left p-2 font-medium">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(indicators).map(([key, value]) => (
|
||||
<tr key={key} className="border-b">
|
||||
<td className="p-2 capitalize">{key}</td>
|
||||
<td className="p-2 font-mono">{value.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(indicators).map(([key, value]) => (
|
||||
<div key={key} className="flex justify-between py-1.5 border-b border-gray-200 last:border-0">
|
||||
<span className="text-gray-600 capitalize">{key}</span>
|
||||
<span className="font-mono font-medium">{value.toFixed(2)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
import { type RouteConfig, index } from "@react-router/dev/routes";
|
||||
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("routes/home.tsx"),
|
||||
index("routes/stocks.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
index("routes/landing.tsx"),
|
||||
route("api/alpaca/account", "routes/api/alpaca/account.ts"),
|
||||
route("stocks", "routes/stocks.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
@@ -1,21 +1,42 @@
|
||||
import type { AlpacaAccount } from "../../../types";
|
||||
import Alpaca from "@alpacahq/alpaca-trade-api";
|
||||
|
||||
const alpaca = new Alpaca({
|
||||
keyId: process.env.ALPACA_API_KEY!,
|
||||
secretKey: process.env.ALPACA_SECRET_KEY!,
|
||||
baseUrl: process.env.ALPACA_BASE_URL || "https://paper-api.alpaca.markets",
|
||||
retryOnError: false,
|
||||
});
|
||||
|
||||
// Mock Alpaca account data – replace with actual API call
|
||||
async function fetchAlpacaAccount(): Promise<AlpacaAccount> {
|
||||
return {
|
||||
cash: 12345.67,
|
||||
buying_power: 8000.0,
|
||||
portfolio_value: 25000.0,
|
||||
};
|
||||
try {
|
||||
console.log("Fetching Alpaca account with key:", process.env.ALPACA_API_KEY?.substring(0, 8) + "...");
|
||||
const account = await alpaca.getAccount();
|
||||
console.log("Alpaca account fetched successfully");
|
||||
return {
|
||||
cash: parseFloat(account.cash),
|
||||
buying_power: parseFloat(account.buying_power),
|
||||
portfolio_value: parseFloat(account.portfolio_value),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Alpaca API fetch error:", error);
|
||||
return {
|
||||
cash: 0,
|
||||
buying_power: 0,
|
||||
portfolio_value: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function loader() {
|
||||
try {
|
||||
const account = await fetchAlpacaAccount();
|
||||
return Response.json(account);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error("Alpaca API error:", error);
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return Response.json(
|
||||
{ error: "Failed to fetch account info" },
|
||||
{ error: `Failed to fetch account info: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Route } from "./+types/home";
|
||||
import { Welcome } from "../welcome/welcome";
|
||||
import AlpacaAccountInfo from "../components/AlpacaAccountInfo";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "New React Router App" },
|
||||
{ name: "description", content: "Welcome to React Router!" },
|
||||
];
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Welcome />
|
||||
<div className="container mx-auto p-4 max-w-2xl">
|
||||
<AlpacaAccountInfo />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Link } from "react-router";
|
||||
import Navbar from "../components/Navbar";
|
||||
import AlpacaAccountInfo from "../components/AlpacaAccountInfo";
|
||||
|
||||
export default function Landing() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
|
||||
<Navbar />
|
||||
{/* Hero Section */}
|
||||
<section className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Welcome to AITrader
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Your AI-powered trading dashboard with real-time market insights and portfolio management.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Account Info Card */}
|
||||
<div className="max-w-md mx-auto">
|
||||
<AlpacaAccountInfo />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+17
-3
@@ -1,9 +1,23 @@
|
||||
import StockViewer from "../components/StockViewer";
|
||||
import Navbar from "../components/Navbar";
|
||||
|
||||
export default function Stocks() {
|
||||
return (
|
||||
<main className="flex items-start justify-center pt-8 pb-4">
|
||||
<StockViewer />
|
||||
</main>
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
|
||||
<Navbar />
|
||||
<div className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Stock Indicators
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Analyze technical indicators for any stock symbol.
|
||||
</p>
|
||||
</div>
|
||||
<StockViewer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+52
-59
@@ -9,81 +9,74 @@ export function Welcome() {
|
||||
<div className="w-[500px] max-w-[100vw] p-4">
|
||||
<img
|
||||
src={logoLight}
|
||||
alt="React Router"
|
||||
alt="AI Trader"
|
||||
className="block w-full dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src={logoDark}
|
||||
alt="React Router"
|
||||
alt="AI Trader"
|
||||
className="hidden w-full dark:block"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-[300px] w-full space-y-6 px-4">
|
||||
<div className="max-w-[400px] w-full space-y-6 px-4">
|
||||
<nav className="rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4">
|
||||
<p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
|
||||
What's next?
|
||||
AI Trader Features
|
||||
</p>
|
||||
<ul>
|
||||
{resources.map(({ href, text, icon }) => (
|
||||
<li key={href}>
|
||||
<a
|
||||
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<li key="stocks">
|
||||
<a
|
||||
href="/stocks"
|
||||
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
{icon}
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<path
|
||||
d="M4 5a1 1 0 011-1h10a1 1 0 010 2H5a1 1 0 01-1-1zM4 10a1 1 0 011-1h10a1 1 0 010 2H5a1 1 0 01-1-1zM4 15a1 1 0 011-1h10a1 1 0 010 2H5a1 1 0 01-1-1z"
|
||||
/>
|
||||
</svg>
|
||||
Stock Indicators
|
||||
</a>
|
||||
</li>
|
||||
<li key="home">
|
||||
<a
|
||||
href="/"
|
||||
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M5 12l5 5 5-5"
|
||||
/>
|
||||
</svg>
|
||||
Home Dashboard
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||
<p>
|
||||
<strong>Stock Indicators:</strong> Get SMA, EMA, RSI, MACD for any stock symbol
|
||||
</p>
|
||||
<p>
|
||||
<strong>Account Info:</strong> View your Alpaca account balance and positions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const resources = [
|
||||
{
|
||||
href: "https://reactrouter.com/docs",
|
||||
text: "React Router Docs",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "https://rmx.as/discord",
|
||||
text: "Join Discord",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="20"
|
||||
viewBox="0 0 24 20"
|
||||
fill="none"
|
||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
||||
>
|
||||
<path
|
||||
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user