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
5 changes: 4 additions & 1 deletion .github/workflows/deploy-static-site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,17 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const distributionId = '${{ steps.get-cloudfront.outputs.distribution_id }}';
const cloudFrontStatus = distributionId ? '✅ Cache invalidated' : '⚠️ No distribution found';

const output = `### 🚀 Static Site Deployed to **${{ steps.set-env.outputs.environment }}** environment

**Preview URL**: https://${{ steps.set-vars.outputs.domain_name }}

**Environment Details**:
- S3 Bucket: \`${{ steps.set-vars.outputs.bucket_name }}\`
- API URL: \`${{ steps.set-vars.outputs.api_base_url }}\`
- CloudFront: ${steps.get-cloudfront.outputs.distribution_id ? '✅ Cache invalidated' : '⚠️ No distribution found'}
- CloudFront: ${cloudFrontStatus}

*Deployed by: @${{ github.actor }}*`;

Expand Down
230 changes: 175 additions & 55 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,133 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Authenticator } from '@aws-amplify/ui-react';
import { signOut } from 'aws-amplify/auth';
import '@aws-amplify/ui-react/styles.css';
import './lib/amplify-config';
import { authenticatedFetch } from './lib/api-client';
import "./lib/amplify-config";
import { AuthProvider } from "@/contexts/auth-context";
import { useAuth } from "@/hooks/use-auth";
import type { Note } from "@/lib/repositories";
import { ApiNotesRepository } from "@/lib/repositories";
import {
MigrationService,
type MigrationResult,
} from "@/lib/migration-service";
import { NotesList } from "@/components/notes-list";
import { NoteForm, type Note } from "@/components/note-form";
import { NoteForm } from "@/components/note-form";
import { SearchBar } from "@/components/search-bar";
import { AuthModal } from "@/components/auth-modal";
import { GuestBanner } from "@/components/guest-banner";
import { MigrationModal } from "@/components/migration-modal";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/hooks/use-toast";
import { FaRegCopyright } from "react-icons/fa";

const endpoint = "/notes";

function AppContent() {
const { toast } = useToast();
const queryClient = useQueryClient();
const { authMode, repository, logout, setAuthMode } = useAuth();

const [searchQuery, setSearchQuery] = useState("");
const [editingNote, setEditingNote] = useState<Note | null>(null);
const [migrationProgress, setMigrationProgress] = useState<{
current: number;
total: number;
} | null>(null);
const [migrationResult, setMigrationResult] =
useState<MigrationResult | null>(null);
const [showMigrationModal, setShowMigrationModal] = useState(false);

// 認証状態が変わったら移行をチェック
useEffect(() => {
if (authMode === "authenticated") {
checkAndMigrate();
} else if (authMode === "guest") {
// ゲストモードに戻ったときもクエリを再実行
queryClient.invalidateQueries({ queryKey: ["notes"] });
}
}, [authMode]);

// 移行チェックと実行
async function checkAndMigrate() {
const migrationService = new MigrationService();
const hasGuestNotes = await migrationService.hasGuestNotes();

if (hasGuestNotes) {
setAuthMode("migrating");
setShowMigrationModal(true);
setMigrationProgress({ current: 0, total: 0 });

try {
const apiRepository = new ApiNotesRepository();
const result = await migrationService.migrateNotes(
apiRepository,
(current, total) => {
setMigrationProgress({ current, total });
}
);

setMigrationResult(result);

if (result.success) {
// 移行成功
toast({
title: "✓ ノートを移行しました",
description: `${result.migratedCount} 件のノートがアカウントに保存されました`,
});

// React Queryのキャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ["notes"] });
setAuthMode("authenticated");

// 成功後、3秒でモーダルを自動的に閉じる
setTimeout(() => {
setShowMigrationModal(false);
setMigrationProgress(null);
setMigrationResult(null);
}, 3000);
} else {
// 一部失敗
toast({
title: "⚠ 一部のノートを移行できませんでした",
description: `${result.failedCount} 件のノートが移行できませんでした`,
variant: "destructive",
});
setAuthMode("authenticated");
}
} catch (error) {
console.error("Migration error:", error);
toast({
title: "エラーが発生しました",
description: "ノートの移行に失敗しました",
variant: "destructive",
});
setAuthMode("authenticated");
}
} else {
// ゲストノートがない場合でも、認証後は必ずクエリを再実行
queryClient.invalidateQueries({ queryKey: ["notes"] });
}
}

// 移行リトライ
function handleMigrationRetry() {
setMigrationResult(null);
setMigrationProgress(null);
checkAndMigrate();
}

// 移行モーダルを閉じる
function closeMigrationModal() {
setShowMigrationModal(false);
setMigrationProgress(null);
setMigrationResult(null);
}

// Fetch notes
// Fetch notes using repository
const { data, isLoading, error } = useQuery<{ notes: Note[] }>({
queryKey: ["notes"],
queryKey: ["notes", authMode, repository.constructor.name],
queryFn: async () => {
const res = await authenticatedFetch(endpoint);
if (!res.ok) throw new Error("Failed to fetch notes");
return res.json();
const notes = await repository.fetchNotes();
return { notes };
},
enabled: authMode !== "migrating",
});

// Add note mutation
Expand All @@ -39,18 +139,13 @@ function AppContent() {
title: string;
content: string;
}) => {
const res = await authenticatedFetch(endpoint, {
method: "POST",
body: JSON.stringify({ title, content }),
});
if (!res.ok) throw new Error("Failed to add note");
return res.json();
return repository.createNote(title, content);
},
onSuccess: () => {
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["notes"] });
toast({
title: "✓ ノートを作成しました",
description: "新しいノートが追加されました",
description: `新しいノートが追加されました:${variables.title}`,
});
},
onError: () => {
Expand All @@ -73,19 +168,14 @@ function AppContent() {
title: string;
content: string;
}) => {
const res = await authenticatedFetch(`${endpoint}/${noteId}`, {
method: "PUT",
body: JSON.stringify({ title, content }),
});
if (!res.ok) throw new Error("Failed to update note");
return res.json();
return repository.updateNote(noteId, title, content);
},
onSuccess: () => {
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["notes"] });
setEditingNote(null);
toast({
title: "✓ ノートを更新しました",
description: "変更が保存されました",
description: `変更が保存されました:${variables.title}`,
});
},
onError: () => {
Expand All @@ -99,17 +189,14 @@ function AppContent() {

// Delete note mutation
const deleteMutation = useMutation({
mutationFn: async (noteId: string) => {
const res = await authenticatedFetch(`${endpoint}/${noteId}`, {
method: "DELETE"
});
if (!res.ok) throw new Error("Failed to delete note");
mutationFn: async ({ noteId }: { noteId: string; title: string }) => {
return repository.deleteNote(noteId);
},
onSuccess: () => {
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["notes"] });
toast({
title: "✓ ノートを削除しました",
description: "ノートが削除されました",
description: `ノートが削除されました:${variables.title}`,
});
},
onError: () => {
Expand Down Expand Up @@ -137,9 +224,22 @@ function AppContent() {

const handleSignOut = async () => {
try {
await signOut();
// ログアウト前に進行中のクエリをキャンセル
await queryClient.cancelQueries({ queryKey: ["notes"] });
await logout();
// クエリをクリアして、ゲストモードで再取得させる
queryClient.removeQueries({ queryKey: ["notes"] });
toast({
title: "✓ ログアウトしました",
description: "またのご利用をお待ちしています",
});
} catch (error) {
console.error('Error signing out:', error);
console.error("Error signing out:", error);
toast({
title: "エラーが発生しました",
description: "ログアウトに失敗しました",
variant: "destructive",
});
}
};

Expand All @@ -155,14 +255,16 @@ function AppContent() {
<div className="mx-auto max-w-5xl px-4 py-8 md:py-12">
{/* Header */}
<header className="mb-12 text-center">
<div className="mb-4 flex justify-end">
<button
onClick={handleSignOut}
className="rounded-md bg-accent/10 px-4 py-2 text-sm font-medium text-accent transition-colors hover:bg-accent/20"
>
サインアウト
</button>
</div>
{authMode !== "guest" && (
<div className="mb-4 flex justify-end gap-3">
<button
onClick={handleSignOut}
className="rounded-md bg-accent/10 px-4 py-2 text-sm font-medium text-accent transition-colors hover:bg-accent/20"
>
ログアウト
</button>
</div>
)}
<div className="mb-3 inline-flex items-center justify-center rounded-full bg-accent/10 px-4 py-1.5">
<span className="text-sm font-medium text-accent">
Productivity Tool
Expand All @@ -176,6 +278,9 @@ function AppContent() {
</p>
</header>

{/* Guest Banner */}
{authMode === "guest" && <GuestBanner />}

{/* Search Bar */}
<div className="mb-8">
<SearchBar
Expand All @@ -201,9 +306,14 @@ function AppContent() {
notes={filteredNotes || []}
isLoading={isLoading}
error={error}
onDelete={(noteId) => deleteMutation.mutate(noteId)}
onDelete={(noteId) => {
const note = filteredNotes?.find((n) => n.noteId === noteId);
if (note) {
deleteMutation.mutate({ noteId, title: note.title });
}
}}
onEdit={setEditingNote}
isDeletingId={deleteMutation.variables}
isDeletingId={deleteMutation.variables?.noteId}
searchQuery={searchQuery}
/>

Expand Down Expand Up @@ -238,18 +348,28 @@ function AppContent() {
</div>
</footer>
</div>

{/* Auth Modal */}
<AuthModal />

{/* Migration Modal */}
<MigrationModal
isOpen={showMigrationModal}
progress={migrationProgress}
result={migrationResult}
onRetry={handleMigrationRetry}
onClose={closeMigrationModal}
/>

<Toaster />
</div>
);
}

export default function App() {
return (
<Authenticator
signUpAttributes={['email']}
loginMechanisms={['email']}
>
{() => <AppContent />}
</Authenticator>
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
Loading