Skip to content

Commit b6d4e5f

Browse files
authored
Merge pull request #21 from OneWalkDev/dev
送信履歴を作成
2 parents 6c8d48a + 01df30f commit b6d4e5f

6 files changed

Lines changed: 268 additions & 1 deletion

File tree

backend/app/Http/Controllers/PostController.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,19 @@ public function receivedHistory(Request $request)
9898

9999
return response()->json($history);
100100
}
101+
102+
public function sentHistory(Request $request)
103+
{
104+
$user = $request->user();
105+
106+
if (!$user) {
107+
return response()->json(['error' => 'Not authenticated'], 401);
108+
}
109+
110+
$perPage = (int) $request->query('per_page', 10);
111+
112+
$history = $this->postService->getSentHistory($user, $perPage);
113+
114+
return response()->json($history);
115+
}
101116
}

backend/app/Repositories/PostRepository.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,12 @@ public function getTodayActiveUsersCount(): int
8686
->distinct('user_id')
8787
->count('user_id');
8888
}
89+
90+
public function getSentByUserPaginated(User $user, int $perPage = 10)
91+
{
92+
return Post::where('user_id', $user->id)
93+
->with(['track.primaryGenre', 'genres'])
94+
->orderByDesc('created_at')
95+
->paginate($perPage);
96+
}
8997
}

backend/app/Services/PostService.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,42 @@ public function getReceivedHistory(User $user, int $perPage = 10): array
247247
],
248248
];
249249
}
250+
251+
public function getSentHistory(User $user, int $perPage = 10): array
252+
{
253+
$paginator = $this->postRepository->getSentByUserPaginated($user, $perPage);
254+
255+
$data = $paginator->getCollection()->map(function ($post) {
256+
return [
257+
'id' => $post->id,
258+
'sent_at' => $post->created_at?->toIso8601String(),
259+
'track' => [
260+
'title' => $post->track->title,
261+
'artist' => $post->track->artist,
262+
'url' => $post->track->url,
263+
'primary_genre' => [
264+
'id' => $post->track->primaryGenre->id,
265+
'name' => $post->track->primaryGenre->name,
266+
'slug' => $post->track->primaryGenre->slug,
267+
],
268+
],
269+
'genres' => $post->genres->map(fn($genre) => [
270+
'id' => $genre->id,
271+
'name' => $genre->name,
272+
'slug' => $genre->slug,
273+
]),
274+
'comment' => $post->comment,
275+
];
276+
})->values();
277+
278+
return [
279+
'data' => $data,
280+
'pagination' => [
281+
'current_page' => $paginator->currentPage(),
282+
'per_page' => $paginator->perPage(),
283+
'last_page' => $paginator->lastPage(),
284+
'total' => $paginator->total(),
285+
],
286+
];
287+
}
250288
}

backend/routes/api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@
2222
Route::get('/posts/{id}/', [PostController::class, 'show'])->middleware('auth:sanctum');
2323
Route::get('/today-received-post/', [PostController::class, 'todayReceivedPost'])->middleware('auth:sanctum');
2424
Route::get('/received-posts/', [PostController::class, 'receivedHistory'])->middleware('auth:sanctum');
25+
Route::get('/sent-posts/', [PostController::class, 'sentHistory'])->middleware('auth:sanctum');
2526
Route::get('/stats/', [PostController::class, 'stats']);
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { AppHeader } from '@/components/layout/AppHeader'
5+
import { useAuth } from '@/contexts/AuthContext'
6+
import { useRouter } from 'next/navigation'
7+
import { motion } from 'motion/react'
8+
import { IoMusicalNotes } from 'react-icons/io5'
9+
import { FaPlay, FaPaperPlane } from 'react-icons/fa'
10+
import { apiRequest } from '@/utils/api'
11+
import { AppFooter } from '@/components/layout/AppFooter'
12+
13+
interface Genre {
14+
id: number
15+
name: string
16+
slug: string
17+
}
18+
19+
interface SentItem {
20+
id: number
21+
sent_at: string
22+
track: {
23+
title: string
24+
artist: string
25+
url: string
26+
primary_genre: Genre
27+
}
28+
genres?: Genre[]
29+
comment?: string
30+
}
31+
32+
export default function SentHistoryPage() {
33+
const { isAuthenticated } = useAuth()
34+
const router = useRouter()
35+
const [items, setItems] = useState<SentItem[]>([])
36+
const [isLoading, setIsLoading] = useState(true)
37+
const [page, setPage] = useState(1)
38+
const [lastPage, setLastPage] = useState(1)
39+
const [total, setTotal] = useState(0)
40+
const perPage = 10
41+
42+
useEffect(() => {
43+
if (!isAuthenticated) {
44+
router.push('/login')
45+
return
46+
}
47+
48+
const fetchHistory = async (pageNum: number) => {
49+
setIsLoading(true)
50+
try {
51+
const response = await apiRequest(`/api/sent-posts/?page=${pageNum}&per_page=${perPage}`)
52+
if (response.ok) {
53+
const data = await response.json()
54+
setItems(data.data || [])
55+
setLastPage(data.pagination?.last_page || 1)
56+
setTotal(data.pagination?.total || 0)
57+
} else {
58+
setItems([])
59+
setLastPage(1)
60+
setTotal(0)
61+
}
62+
} catch (error) {
63+
console.error('履歴取得エラー:', error)
64+
setItems([])
65+
setLastPage(1)
66+
setTotal(0)
67+
} finally {
68+
setIsLoading(false)
69+
}
70+
}
71+
72+
fetchHistory(page)
73+
}, [isAuthenticated, router, page])
74+
75+
const handleOpen = (url: string) => {
76+
if (url) window.open(url, '_blank')
77+
}
78+
79+
return (
80+
<>
81+
<AppHeader />
82+
<main className="relative min-h-screen px-4 py-12 bg-gradient-to-br from-[#fff1d7] via-[#ffe7f7] to-[#dff6ff] overflow-hidden text-slate-900">
83+
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
84+
<div className="absolute -left-16 -top-10 w-64 h-64 bg-gradient-to-br from-amber-200/70 to-pink-200/60 rounded-full blur-3xl" />
85+
<div className="absolute right-0 top-16 w-72 h-72 bg-gradient-to-br from-cyan-200/60 via-sky-200/50 to-emerald-200/50 rounded-full blur-3xl" />
86+
<div className="absolute left-1/3 -bottom-24 w-80 h-80 bg-gradient-to-br from-emerald-100/70 via-white to-amber-200/60 rounded-full blur-3xl" />
87+
</div>
88+
89+
<div className="max-w-5xl mx-auto relative z-10">
90+
<motion.div
91+
initial={{ opacity: 0, y: 20 }}
92+
animate={{ opacity: 1, y: 0 }}
93+
transition={{ duration: 0.6 }}
94+
className="text-center mb-10"
95+
>
96+
<div className="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-white/80 border border-pink-100 text-pink-700 shadow-sm">
97+
<FaPaperPlane />
98+
送信履歴
99+
</div>
100+
<h1 className="text-3xl sm:text-4xl font-black text-slate-900 mt-4">
101+
これまで送った曲
102+
</h1>
103+
<p className="text-slate-600 mt-2">あなたが誰かに送った曲の一覧</p>
104+
</motion.div>
105+
106+
<motion.div
107+
initial={{ opacity: 0, y: 10 }}
108+
animate={{ opacity: 1, y: 0 }}
109+
transition={{ duration: 0.5, delay: 0.1 }}
110+
className="bg-white/85 backdrop-blur-xl rounded-3xl border border-white/60 shadow-2xl p-6"
111+
>
112+
{isLoading ? (
113+
<div className="text-center text-slate-600 py-8">読み込み中...</div>
114+
) : items.length === 0 ? (
115+
<div className="text-center text-slate-600 py-8">
116+
まだ送った曲がありません。
117+
</div>
118+
) : (
119+
<div className="space-y-4">
120+
{items.map((item) => (
121+
<motion.div
122+
key={item.id}
123+
initial={{ opacity: 0, y: 8 }}
124+
animate={{ opacity: 1, y: 0 }}
125+
transition={{ duration: 0.3 }}
126+
className="flex flex-col sm:flex-row sm:items-center gap-4 p-4 rounded-2xl border border-white/60 bg-white/80 shadow-sm"
127+
>
128+
<div className="flex-shrink-0">
129+
<div className="h-12 w-12 rounded-2xl bg-gradient-to-br from-pink-500 via-purple-500 to-indigo-500 text-white flex items-center justify-center text-2xl shadow-md">
130+
<IoMusicalNotes />
131+
</div>
132+
</div>
133+
<div className="flex-1 space-y-1">
134+
<div className="flex flex-wrap items-center gap-2">
135+
<p className="text-lg font-semibold text-slate-900">{item.track.title}</p>
136+
<span className="text-sm text-slate-500">/ {item.track.artist}</span>
137+
</div>
138+
<div className="flex flex-wrap gap-2">
139+
{(item.genres && item.genres.length ? item.genres : [item.track.primary_genre]).map((genre) => (
140+
<span
141+
key={genre.id}
142+
className="px-3 py-1 rounded-full bg-gradient-to-r from-pink-100 via-purple-100 to-indigo-100 border border-white/60 text-xs font-semibold text-slate-800"
143+
>
144+
{genre.name}
145+
</span>
146+
))}
147+
</div>
148+
{item.comment && (
149+
<p className="text-sm text-slate-600 bg-white/80 border border-white/60 rounded-lg px-3 py-2">
150+
{item.comment}
151+
</p>
152+
)}
153+
<p className="text-xs text-slate-400">
154+
送信日時: {new Date(item.sent_at).toLocaleString('ja-JP')}
155+
</p>
156+
</div>
157+
<div className="flex-shrink-0 flex gap-2">
158+
<button
159+
onClick={() => handleOpen(item.track.url)}
160+
className="px-4 py-2 rounded-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white text-sm font-semibold shadow-md hover:shadow-lg transition-all flex items-center gap-2"
161+
>
162+
<FaPlay />
163+
再生
164+
</button>
165+
</div>
166+
</motion.div>
167+
))}
168+
169+
<div className="flex items-center justify-between pt-4">
170+
<div className="text-sm text-slate-600">
171+
{total}件 / {page}ページ目
172+
</div>
173+
<div className="flex gap-2">
174+
<button
175+
onClick={() => setPage((p) => Math.max(1, p - 1))}
176+
disabled={page <= 1}
177+
className="px-4 py-2 rounded-full bg-white text-slate-800 border border-pink-100 shadow-sm disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-md transition-all text-sm font-semibold"
178+
>
179+
前へ
180+
</button>
181+
<button
182+
onClick={() => setPage((p) => Math.min(lastPage, p + 1))}
183+
disabled={page >= lastPage}
184+
className="px-4 py-2 rounded-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 text-white shadow-sm disabled:opacity-50 disabled:cursor-not-allowed hover:shadow-md transition-all text-sm font-semibold"
185+
>
186+
次へ
187+
</button>
188+
</div>
189+
</div>
190+
</div>
191+
)}
192+
</motion.div>
193+
</div>
194+
</main>
195+
<AppFooter />
196+
</>
197+
)
198+
}

frontend/src/app/settings/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Link from 'next/link'
44
import { motion } from 'motion/react'
55
import { AppHeader } from '@/components/layout/AppHeader'
66
import { IoMusicalNotes, IoSettingsSharp } from 'react-icons/io5'
7-
import { FaHistory, FaHeart } from 'react-icons/fa'
7+
import { FaHistory, FaHeart, FaPaperPlane } from 'react-icons/fa'
88
import { AppFooter } from '@/components/layout/AppFooter'
99

1010
export default function SettingsPage() {
@@ -23,6 +23,13 @@ export default function SettingsPage() {
2323
icon: <FaHistory />,
2424
color: 'from-sky-500 via-cyan-500 to-emerald-500'
2525
},
26+
{
27+
title: '送信履歴',
28+
desc: 'これまで送った曲を確認',
29+
href: '/sent/history',
30+
icon: <FaPaperPlane />,
31+
color: 'from-pink-500 via-purple-500 to-indigo-500'
32+
},
2633
]
2734

2835
return (

0 commit comments

Comments
 (0)