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 frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const createNextIntlPlugin = require("next-intl/plugin");

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

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
Expand All @@ -12,4 +16,4 @@ const nextConfig = {
},
};

module.exports = nextConfig;
module.exports = withNextIntl(nextConfig);
1 change: 1 addition & 0 deletions frontend/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": "^4.11.2",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { useTranslations } from "next-intl";
import { useState } from "react";
import { Navbar } from "@/components/Navbar";
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";

// TODO: Fetch real data from contract
Expand Down Expand Up @@ -33,8 +34,16 @@ const MOCK_GROUP = {
};

export default function GroupDetailPage() {
const t = useTranslations("GroupDetailPage");
const [showContributeModal, setShowContributeModal] = useState(false);
const group = MOCK_GROUP;
const statusLabels: Record<string, string> = {
Forming: t("statusLabels.forming"),
Active: t("statusLabels.active"),
Completed: t("statusLabels.completed"),
Disputed: t("statusLabels.disputed"),
Paused: t("statusLabels.paused"),
};

return (
<>
Expand All @@ -43,7 +52,7 @@ 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
{formatAmount(group.contributionAmount)} {t("tokensPerCycle")}
</p>
</div>

Expand All @@ -67,53 +76,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("groupInfo")}
</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">
{statusLabels[String(group.status)] || String(group.status)}
</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
{group.cycleLength / 86400} {t("days")}
</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
{t("tokens")}
</dd>
</div>
</dl>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useTranslations } from "next-intl";
import { Navbar } from "@/components/Navbar";
import { CreateGroupForm } from "@/components/CreateGroupForm";

export default function NewGroupPage() {
const t = useTranslations("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
{t("title")}
</h1>
<CreateGroupForm />
</main>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { useTranslations } from "next-intl";
import { Navbar } from "@/components/Navbar";
import { GroupCard } from "@/components/GroupCard";
import { SavingsGroup, GroupStatus } from "@sorosave/sdk";

// Placeholder data for development will be replaced with contract queries
// Placeholder data for development - will be replaced with contract queries
const PLACEHOLDER_GROUPS: SavingsGroup[] = [
{
id: 1,
Expand Down Expand Up @@ -39,6 +40,7 @@ const PLACEHOLDER_GROUPS: SavingsGroup[] = [
];

export default function GroupsPage() {
const t = useTranslations("GroupsPage");
// TODO: Replace with actual contract queries
const groups = PLACEHOLDER_GROUPS;

Expand All @@ -47,13 +49,11 @@ export default function GroupsPage() {
<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!
</div>
<div className="text-center py-12 text-gray-500">{t("empty")}</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{groups.map((group) => (
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Metadata } from "next";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { Providers } from "../providers";
import { routing, type Locale } from "@/i18n/routing";
import "../globals.css";

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

export async function generateMetadata({
params,
}: {
params: { locale: Locale };
}): Promise<Metadata> {
const t = await getTranslations({
locale: params.locale,
namespace: "Metadata",
});

return {
title: t("title"),
description: t("description"),
};
}

export default function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
if (!hasLocale(routing.locales, params.locale)) {
notFound();
}

return (
<html lang={params.locale}>
<body className="min-h-screen bg-gray-50">
<NextIntlClientProvider>
<Providers>{children}</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
91 changes: 28 additions & 63 deletions frontend/src/app/page.tsx → frontend/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import Link from "next/link";
import { useTranslations } from "next-intl";
import { Link } from "@/i18n/navigation";
import { Navbar } from "@/components/Navbar";

const stepKeys = ["create", "join", "contribute", "receive"] as const;
const featureKeys = ["trustless", "transparent", "lowCost"] as const;

export default function Home() {
const t = useTranslations("HomePage");

return (
<>
<Navbar />
Expand All @@ -11,25 +17,23 @@ export default function Home() {
<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">
Decentralized Group Savings for Everyone
{t("hero.title")}
</h1>
<p className="text-xl text-primary-100 mb-8">
SoroSave brings the traditional rotating savings model (ajo,
susu, chit fund) to the Stellar blockchain. Trustless,
transparent, and accessible.
{t("hero.description")}
</p>
<div className="flex space-x-4">
<Link
href="/groups"
className="bg-white text-primary-700 px-6 py-3 rounded-lg font-semibold hover:bg-primary-50 transition-colors"
>
Browse Groups
{t("hero.browseGroups")}
</Link>
<Link
href="/groups/new"
className="border-2 border-white text-white px-6 py-3 rounded-lg font-semibold hover:bg-white/10 transition-colors"
>
Create a Group
{t("hero.createGroup")}
</Link>
</div>
</div>
Expand All @@ -40,43 +44,20 @@ export default function Home() {
<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">
How It Works
{t("how.title")}
</h2>
<div className="grid md:grid-cols-4 gap-8">
{[
{
step: "1",
title: "Create a Group",
description:
"Set contribution amount, cycle length, and max members. You become the admin.",
},
{
step: "2",
title: "Members Join",
description:
"Share the group link. Members join until the group is full or the admin starts it.",
},
{
step: "3",
title: "Contribute Each Cycle",
description:
"Every member contributes the fixed amount each round. Smart contract enforces rules.",
},
{
step: "4",
title: "Receive the Pot",
description:
"Each round, one member receives the full pot. Continues until everyone has received.",
},
].map((item) => (
<div key={item.step} className="text-center">
{stepKeys.map((stepKey, index) => (
<div key={stepKey} 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}
{index + 1}
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{item.title}
{t(`how.steps.${stepKey}.title`)}
</h3>
<p className="text-gray-600 text-sm">{item.description}</p>
<p className="text-gray-600 text-sm">
{t(`how.steps.${stepKey}.description`)}
</p>
</div>
))}
</div>
Expand All @@ -87,34 +68,20 @@ export default function Home() {
<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">
Why SoroSave?
{t("features.title")}
</h2>
<div className="grid md:grid-cols-3 gap-8">
{[
{
title: "Trustless",
description:
"Smart contracts enforce contributions and payouts. No middleman needed.",
},
{
title: "Transparent",
description:
"All transactions are on-chain. Every member can verify the group state.",
},
{
title: "Low Cost",
description:
"Built on Soroban (Stellar). Transaction fees are a fraction of a cent.",
},
].map((feature) => (
{featureKeys.map((feature) => (
<div
key={feature.title}
key={feature}
className="bg-white p-6 rounded-xl shadow-sm"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{feature.title}
{t(`features.items.${feature}.title`)}
</h3>
<p className="text-gray-600">{feature.description}</p>
<p className="text-gray-600">
{t(`features.items.${feature}.description`)}
</p>
</div>
))}
</div>
Expand All @@ -125,9 +92,7 @@ export default function Home() {
<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">
Open-source decentralized group savings protocol on Soroban.
</p>
<p className="text-sm">{t("footer.description")}</p>
<div className="mt-4 space-x-4 text-sm">
<a
href="https://github.com/big14way/sorosave"
Expand All @@ -136,7 +101,7 @@ export default function Home() {
GitHub
</a>
<a href="#" className="hover:text-white">
Docs
{t("footer.docs")}
</a>
<a href="#" className="hover:text-white">
Discord
Expand Down
Loading