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
6 changes: 5 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const createNextIntlPlugin = require("next-intl/plugin");

const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");

const nextConfig = {
reactStrictMode: true,
webpack: (config) => {
Expand All @@ -12,4 +16,4 @@ const nextConfig = {
},
};

module.exports = nextConfig;
module.exports = withNextIntl(nextConfig);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@sorosave/sdk": "workspace:*",
"@stellar/freighter-api": "^2.0.0",
"next": "^14.2.0",
"next-intl": "^3.26.5",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { MemberList } from "@/components/MemberList";
import { RoundProgress } from "@/components/RoundProgress";
import { ContributeModal } from "@/components/ContributeModal";
import { useState } from "react";
import { formatAmount, GroupStatus } from "@sorosave/sdk";
import { formatAmount, getStatusLabel, GroupStatus } from "@sorosave/sdk";
import { useTranslations } from "next-intl";

// TODO: Fetch real data from contract
const MOCK_GROUP = {
Expand All @@ -32,9 +33,21 @@ const MOCK_GROUP = {
createdAt: 1700000000,
};

const statusMessageKeys: Record<string, string> = {
Forming: "forming",
Active: "active",
Completed: "completed",
Disputed: "disputed",
Paused: "paused",
};

export default function GroupDetailPage() {
const [showContributeModal, setShowContributeModal] = useState(false);
const group = MOCK_GROUP;
const t = useTranslations("groupDetail");
const common = useTranslations("common");
const status = useTranslations("status");
const statusLabel = getStatusLabel(group.status);

return (
<>
Expand All @@ -43,7 +56,9 @@ export default function GroupDetailPage() {
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">{group.name}</h1>
<p className="text-gray-600 mt-1">
{formatAmount(group.contributionAmount)} tokens per cycle
{t("tokensPerCycle", {
amount: formatAmount(group.contributionAmount),
})}
</p>
</div>

Expand All @@ -67,53 +82,55 @@ export default function GroupDetailPage() {
<div className="space-y-4">
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Actions
{t("actions")}
</h3>
<div className="space-y-3">
{group.status === GroupStatus.Active && (
<button
onClick={() => setShowContributeModal(true)}
className="w-full bg-primary-600 text-white py-3 rounded-lg font-medium hover:bg-primary-700 transition-colors"
>
Contribute
{t("contribute")}
</button>
)}
{group.status === GroupStatus.Forming && (
<button className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
Join Group
{t("joinGroup")}
</button>
)}
</div>
</div>

<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-sm font-semibold text-gray-900 mb-3">
Group Info
{t("info")}
</h3>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-500">Status</dt>
<dd className="font-medium text-gray-900">{group.status}</dd>
<dt className="text-gray-500">{t("status")}</dt>
<dd className="font-medium text-gray-900">
{status(statusMessageKeys[statusLabel] ?? "unknown")}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Members</dt>
<dt className="text-gray-500">{t("members")}</dt>
<dd className="font-medium text-gray-900">
{group.members.length}/{group.maxMembers}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Cycle</dt>
<dt className="text-gray-500">{t("cycle")}</dt>
<dd className="font-medium text-gray-900">
{group.cycleLength / 86400} days
{t("days", { count: group.cycleLength / 86400 })}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Pot Size</dt>
<dt className="text-gray-500">{t("potSize")}</dt>
<dd className="font-medium text-gray-900">
{formatAmount(
group.contributionAmount * BigInt(group.members.length)
)}{" "}
tokens
{common("tokens")}
</dd>
</div>
</dl>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Navbar } from "@/components/Navbar";
import { CreateGroupForm } from "@/components/CreateGroupForm";
import { getMessages } from "@/i18n/messages";

export default function NewGroupPage({ params }: { params: { locale: string } }) {
const messages = getMessages(params.locale).newGroup;

export default function NewGroupPage() {
return (
<>
<Navbar />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 className="text-2xl font-bold text-gray-900 mb-8">
Create a Savings Group
{messages.title}
</h1>
<CreateGroupForm />
</main>
Expand Down
6 changes: 4 additions & 2 deletions src/app/groups/page.tsx → src/app/[locale]/groups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Navbar } from "@/components/Navbar";
import { GroupCard } from "@/components/GroupCard";
import { SavingsGroup, GroupStatus } from "@sorosave/sdk";
import { useTranslations } from "next-intl";

// Placeholder data for development — will be replaced with contract queries
const PLACEHOLDER_GROUPS: SavingsGroup[] = [
Expand Down Expand Up @@ -41,18 +42,19 @@ const PLACEHOLDER_GROUPS: SavingsGroup[] = [
export default function GroupsPage() {
// TODO: Replace with actual contract queries
const groups = PLACEHOLDER_GROUPS;
const t = useTranslations("groups");

return (
<>
<Navbar />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">Savings Groups</h1>
<h1 className="text-2xl font-bold text-gray-900">{t("title")}</h1>
</div>

{groups.length === 0 ? (
<div className="text-center py-12 text-gray-500">
No groups found. Create the first one!
{t("empty")}
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
Expand Down
32 changes: 32 additions & 0 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextIntlClientProvider } from "next-intl";
import { setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { getMessages } from "@/i18n/messages";
import { isLocale, locales } from "@/i18n/config";

export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}

export default function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!isLocale(params.locale)) {
notFound();
}

setRequestLocale(params.locale);

return (
<NextIntlClientProvider
locale={params.locale}
messages={getMessages(params.locale)}
>
{children}
</NextIntlClientProvider>
);
}
111 changes: 111 additions & 0 deletions src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Link from "next/link";
import { Navbar } from "@/components/Navbar";
import { getLocale, getMessages } from "@/i18n/messages";

export default function Home({ params }: { params: { locale: string } }) {
const locale = getLocale(params.locale);
const messages = getMessages(locale).home;

return (
<>
<Navbar />
<main>
{/* Hero */}
<section className="bg-gradient-to-br from-primary-600 to-primary-800 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<div className="max-w-3xl">
<h1 className="text-4xl sm:text-5xl font-bold mb-6">
{messages.hero.title}
</h1>
<p className="text-xl text-primary-100 mb-8">
{messages.hero.description}
</p>
<div className="flex space-x-4">
<Link
href={`/${locale}/groups`}
className="bg-white text-primary-700 px-6 py-3 rounded-lg font-semibold hover:bg-primary-50 transition-colors"
>
{messages.hero.browseGroups}
</Link>
<Link
href={`/${locale}/groups/new`}
className="border-2 border-white text-white px-6 py-3 rounded-lg font-semibold hover:bg-white/10 transition-colors"
>
{messages.hero.createGroup}
</Link>
</div>
</div>
</div>
</section>

{/* How it works */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-gray-900 text-center mb-12">
{messages.how.title}
</h2>
<div className="grid md:grid-cols-4 gap-8">
{Object.values(messages.how.steps).map((item) => (
<div key={item.step} className="text-center">
<div className="w-12 h-12 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center text-xl font-bold mx-auto mb-4">
{item.step}
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{item.title}
</h3>
<p className="text-gray-600 text-sm">{item.description}</p>
</div>
))}
</div>
</div>
</section>

{/* Features */}
<section className="bg-gray-100 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-gray-900 text-center mb-12">
{messages.features.title}
</h2>
<div className="grid md:grid-cols-3 gap-8">
{Object.values(messages.features.items).map((feature) => (
<div
key={feature.title}
className="bg-white p-6 rounded-xl shadow-sm"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{feature.title}
</h3>
<p className="text-gray-600">{feature.description}</p>
</div>
))}
</div>
</div>
</section>

{/* Footer */}
<footer className="bg-gray-900 text-gray-400 py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-lg font-semibold text-white mb-2">SoroSave</p>
<p className="text-sm">
{messages.footer.description}
</p>
<div className="mt-4 space-x-4 text-sm">
<a
href="https://github.com/big14way/sorosave"
className="hover:text-white"
>
GitHub
</a>
<a href="#" className="hover:text-white">
{messages.footer.docs}
</a>
<a href="#" className="hover:text-white">
{messages.footer.discord}
</a>
</div>
</div>
</footer>
</main>
</>
);
}
Loading