v0.devとlaravel12 starter kitを組合せて何か作ってみる(プロジェクト管理アプリとか)
まずは雛形を作る
https://v0.dev/ にログイン。アカウントは必須なので適当に作成してください。以下のプロンプトでchatを開始。
非常に適当なプロンプトに合せて、ささっと作ってくれる。これをデプロイしたので見たい人はどうぞ https://v0-modern-project-dashboard-bhbjcu.vercel.app/
コンポーネントを別ファイルに分割指示
初回は全てのファイルがpage.tsxに集約されていて面倒だったのでこれを分割した
すると見た目の変化は無いものの以下のように分割してくれた
これをベースに移植していってみよう
starter kitのインストール
割愛。 詳しくは
からとか。
ともあれ、laravel new --react
とかで適当にセットアップ
認証画面は今回はカスタマイズしない
ダッシュボードは以下の通り
このplaceholderまみれのダッシュボードを改変する
レイアウト
今回v0が作ってきたレイアウトはトップにメニューがおかれているものであるがstarter kitのdefaultはサイドバー型式である。これは実は変更することもできる。
@@ -1,4 +1,4 @@
-import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout';
+import AppLayoutTemplate from '@/layouts/app/app-header-layout';
import { type BreadcrumbItem } from '@/types';
import { type ReactNode } from 'react';
headerレイアウトに変更した
レイアウトのメニューは今回弄らないが必要に応じて当然項目を変更する事も可能だ。
v0.devが作ってきたコード
v0.devが作ってきたコード
import Link from "next/link"
import { CheckCircle, Clock, LayoutDashboard, Plus, Users } from "lucide-react"
import { ActivityChart } from "@/components/activity-chart"
import { ProjectCard } from "@/components/project-card"
import { TaskItem } from "@/components/task-item"
import { TeamMemberCard } from "@/components/team-member-card"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function Dashboard() {
return (
<div className="flex min-h-screen w-full flex-col">
<div className="border-b">
<div className="flex h-16 items-center px-4 md:px-6">
<div className="flex items-center gap-2 font-semibold">
<LayoutDashboard className="h-6 w-6" />
<span>ProjectHub</span>
</div>
<nav className="ml-auto flex items-center gap-4 sm:gap-6">
<Button variant="ghost" size="sm" asChild>
<Link href="#">ダッシュボード</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="#">プロジェクト</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="#">タスク</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="#">チーム</Link>
</Button>
<Button variant="ghost" size="sm" asChild>
<Link href="#">設定</Link>
</Button>
</nav>
</div>
</div>
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">ダッシュボード</h2>
<div className="flex items-center gap-2">
<Button>
<Plus className="mr-2 h-4 w-4" />
新規プロジェクト
</Button>
</div>
</div>
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">概要</TabsTrigger>
<TabsTrigger value="projects">プロジェクト</TabsTrigger>
<TabsTrigger value="tasks">タスク</TabsTrigger>
<TabsTrigger value="team">チーム</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">進行中のプロジェクト</CardTitle>
<LayoutDashboard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">前月比 +2.5%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">完了したタスク</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">86</div>
<p className="text-xs text-muted-foreground">前週比 +10.2%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">チームメンバー</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24</div>
<p className="text-xs text-muted-foreground">先月から +4 名</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">期限間近</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8</div>
<p className="text-xs text-muted-foreground">今週中に期限</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>プロジェクト進捗状況</CardTitle>
</CardHeader>
<CardContent className="pl-2">
<ActivityChart />
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>最近のアクティビティ</CardTitle>
<CardDescription>過去7日間のアクティビティ</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center">
<div className="mr-2 h-2 w-2 rounded-full bg-green-500" />
<div className="grid gap-1">
<p className="text-sm font-medium leading-none">
ウェブサイトリニューアルプロジェクト - タスク完了
</p>
<p className="text-sm text-muted-foreground">田中さんが「デザインレビュー」を完了しました</p>
</div>
<div className="ml-auto text-sm text-muted-foreground">2時間前</div>
</div>
<div className="flex items-center">
<div className="mr-2 h-2 w-2 rounded-full bg-blue-500" />
<div className="grid gap-1">
<p className="text-sm font-medium leading-none">モバイルアプリ開発 - 新規タスク</p>
<p className="text-sm text-muted-foreground">佐藤さんが「UI実装」タスクを追加しました</p>
</div>
<div className="ml-auto text-sm text-muted-foreground">5時間前</div>
</div>
<div className="flex items-center">
<div className="mr-2 h-2 w-2 rounded-full bg-yellow-500" />
<div className="grid gap-1">
<p className="text-sm font-medium leading-none">マーケティングキャンペーン - 期限更新</p>
<p className="text-sm text-muted-foreground">鈴木さんが期限を1週間延長しました</p>
</div>
<div className="ml-auto text-sm text-muted-foreground">昨日</div>
</div>
<div className="flex items-center">
<div className="mr-2 h-2 w-2 rounded-full bg-purple-500" />
<div className="grid gap-1">
<p className="text-sm font-medium leading-none">新規プロジェクト作成</p>
<p className="text-sm text-muted-foreground">
山田さんが「ECサイト最適化」プロジェクトを作成しました
</p>
</div>
<div className="ml-auto text-sm text-muted-foreground">2日前</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="projects" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<ProjectCard
title="ウェブサイトリニューアル"
description="企業サイトの全面リニューアル"
progress={75}
dueDate="2024年5月15日"
members={5}
tasks={24}
completedTasks={18}
/>
<ProjectCard
title="モバイルアプリ開発"
description="iOSとAndroid向けの新アプリ開発"
progress={45}
dueDate="2024年7月30日"
members={8}
tasks={36}
completedTasks={16}
/>
<ProjectCard
title="マーケティングキャンペーン"
description="Q2向け販促キャンペーン"
progress={90}
dueDate="2024年4月30日"
members={4}
tasks={12}
completedTasks={11}
/>
<ProjectCard
title="ECサイト最適化"
description="コンバージョン率向上のための改善"
progress={20}
dueDate="2024年8月10日"
members={6}
tasks={28}
completedTasks={6}
/>
<ProjectCard
title="社内システム刷新"
description="レガシーシステムの刷新"
progress={60}
dueDate="2024年9月25日"
members={10}
tasks={42}
completedTasks={25}
/>
<Card className="flex h-full items-center justify-center p-8">
<Button variant="ghost" className="h-20 w-20 rounded-full">
<Plus className="h-10 w-10" />
<span className="sr-only">新規プロジェクト追加</span>
</Button>
</Card>
</div>
</TabsContent>
<TabsContent value="tasks" className="space-y-4">
<div className="grid gap-4">
<Card>
<CardHeader>
<CardTitle>タスク一覧</CardTitle>
<CardDescription>あなたに割り当てられたタスク</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<TaskItem
title="デザインレビュー"
project="ウェブサイトリニューアル"
priority="高"
dueDate="今日"
status="進行中"
/>
<TaskItem
title="API実装"
project="モバイルアプリ開発"
priority="中"
dueDate="明日"
status="未着手"
/>
<TaskItem
title="SNS投稿作成"
project="マーケティングキャンペーン"
priority="低"
dueDate="3日後"
status="進行中"
/>
<TaskItem
title="パフォーマンス分析"
project="ECサイト最適化"
priority="中"
dueDate="1週間後"
status="未着手"
/>
<TaskItem
title="要件定義"
project="社内システム刷新"
priority="高"
dueDate="2日後"
status="レビュー中"
/>
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
<Plus className="mr-2 h-4 w-4" />
新規タスク
</Button>
</CardFooter>
</Card>
</div>
</TabsContent>
<TabsContent value="team" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<TeamMemberCard
name="田中 太郎"
role="プロジェクトマネージャー"
email="tanaka@example.com"
avatar="/placeholder.svg?height=100&width=100"
activeProjects={3}
completedTasks={24}
/>
<TeamMemberCard
name="佐藤 花子"
role="シニアデザイナー"
email="sato@example.com"
avatar="/placeholder.svg?height=100&width=100"
activeProjects={2}
completedTasks={18}
/>
<TeamMemberCard
name="鈴木 一郎"
role="フロントエンド開発者"
email="suzuki@example.com"
avatar="/placeholder.svg?height=100&width=100"
activeProjects={4}
completedTasks={32}
/>
<TeamMemberCard
name="山田 優子"
role="マーケティングスペシャリスト"
email="yamada@example.com"
avatar="/placeholder.svg?height=100&width=100"
activeProjects={2}
completedTasks={15}
/>
</div>
</TabsContent>
</Tabs>
</div>
</div>
)
}
長いのでおりたたんだのであるが、これを移植していく。
dashboardの改造
まず必要なところを取り出してみる。おそらく以下のようになるはずだ
import { type BreadcrumbItem } from '@/types';
import { Head } from '@inertiajs/react';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
},
];
export default function Dashboard() {
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Dashboard" />
</AppLayout>
);
}
タイトルと新規作成ボタン
あとはチマチマ移動していくだけ
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head } from '@inertiajs/react';
import { Button } from "@/components/ui/button"
import { Plus } from 'lucide-react';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
},
];
export default function Dashboard() {
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Dashboard" />
<div className="flex-1 space-y-4 p-4 pt-6 md:p-8">
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight">ダッシュボード</h2>
<div className="flex items-center gap-2">
<Button>
<Plus className="mr-2 h-4 w-4" />
新規プロジェクト
</Button>
</div>
</div>
</div>
</AppLayout>
);
}
shadcn/ui
の部分は何も考えず普通にimportできるのがよいところだろう
タブの部位
これは実は
<Tabs defaultValue="overview" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">概要</TabsTrigger>
<TabsTrigger value="projects">プロジェクト</TabsTrigger>
<TabsTrigger value="tasks">タスク</TabsTrigger>
<TabsTrigger value="team">チーム</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
</TabsContent>
<TabsContent value="projects" className="space-y-4">
</TabsContent>
<TabsContent value="tasks" className="space-y-4">
</TabsContent>
<TabsContent value="teams" className="space-y-4">
</TabsContent>
</Tabs>
このような構造になっている。
これをそのまま持っていって
npx shadcn@latest add tabs
とかで必要なコンポーネントを追加してゆく
stats部分
まあようするにこの部分であるが見た目がほぼ共通化しているのとバックエンドから送信するように改造してみてもいいだろう
@@ -9,7 +9,34 @@
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {
- return Inertia::render('dashboard');
+ return Inertia::render('dashboard', [
+ 'stats' => [
+ 'projects' => [
+ 'title' => '進行中のプロジェクト',
+ 'icon' => 'dashboard',
+ 'value' => 12,
+ 'note' => '前月比 +2.5%',
+ ],
+ 'tasks' => [
+ 'title' => '完了したタスク',
+ 'icon' => 'check',
+ 'value' => 86,
+ 'note' => '前週比 +10.2%',
+ ],
+ 'members' => [
+ 'title' => 'チームメンバー',
+ 'icon' => 'users',
+ 'value' => 24,
+ 'note' => '先月から +4 名',
+ ],
+ 'due' => [
+ 'title' => '期限間近',
+ 'icon' => 'clock',
+ 'value' => 8,
+ 'note' => '今週中に期限',
+ ],
+ ],
+ ]);
})->name('dashboard');
});
フロントエンド
@@ -1,9 +1,10 @@
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head } from '@inertiajs/react';
-import { Button } from "@/components/ui/button"
-import { Plus } from 'lucide-react';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { CheckCircle, Clock, LayoutDashboard, Plus, Users } from 'lucide-react';
const breadcrumbs: BreadcrumbItem[] = [
{
@@ -12,7 +13,14 @@ const breadcrumbs: BreadcrumbItem[] = [
},
];
-export default function Dashboard() {
+const iconMap = {
+ dashboard: <LayoutDashboard className="text-muted-foreground h-4 w-4" />,
+ check: <CheckCircle className="text-muted-foreground h-4 w-4" />,
+ users: <Users className="text-muted-foreground h-4 w-4" />,
+ clock: <Clock className="text-muted-foreground h-4 w-4" />,
+};
+
+export default function Dashboard({ stats }: { stats: any }) {
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Dashboard" />
@@ -34,13 +42,25 @@ export default function Dashboard() {
<TabsTrigger value="team">チーム</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {Object.entries(stats).map(([key, stat]) => (
+ <Card key={key}>
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
+ {iconMap[stat.icon]}
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{stat.value}</div>
+ <p className="text-muted-foreground text-xs">{stat.note}</p>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
</TabsContent>
- <TabsContent value="projects" className="space-y-4">
- </TabsContent>
- <TabsContent value="tasks" className="space-y-4">
- </TabsContent>
- <TabsContent value="teams" className="space-y-4">
- </TabsContent>
+
+ <TabsContent value="projects" className="space-y-4"></TabsContent>
+ <TabsContent value="tasks" className="space-y-4"></TabsContent>
+ <TabsContent value="teams" className="space-y-4"></TabsContent>
</Tabs>
</div>
</AppLayout>
(すんません、typeは適当にany
にしちゃいました)
という感じで進めればok
フルに移植すると無駄に記事が長くなるので、zennの記事で指針を示すだけならこの辺でokと思うけどまあこんな感じで進める。もちろんこのガワだけでは進まないだろう、たとえば新規プロジェクトを押しても無反応である。これは機能がないからそれはそう。
反応は無い
nextjsを使っていればもうちょっとv0.devの結果をそのまま採用できるのであるが、どのみちinertia.jsだとコアの部分は崩壊してくるので、v0.devのchatを維持する意味はあんまり無くなってくる、が、もうちょいv0.devのchatで粘ってみる。以下のプロンプトでチャットを継続する
新規プロジェクトを押したときに適切なフォームをモーダルしてください
などの指示を送ってみると以下のようになった
カラーリングなど
ブルー基調。tailwind.config.tsなどが生成された
v0.dev内で完結してると便利なこと
履歴がしっかり記録されているので行き来出来る
差分を確認する事も可能
冒頭で行ったようにv0.devの内容はまるごとそのままvercelにワンクリックにてデプロイできる
まとめ
アイデアがあればとりあえずv0.devにぶん投げた方が素人がデザインするより流石に綺麗なものが上がってくるので、それをベースにしてwebを組むとよいし、laravel12 starter kit(react)に関しては最初からshadcn/ui
になっているため見た目を移植する事に関しては頑張ってつまみ出してくれば、ほぼ100%の移植精度をキープできるんじゃないかと思う。next linkとかはもちろんinertia linkに変換しないといけないしバックエンドのデーター送信はあるっちゃあるんだけど、それはどのみちそのままv0.devで作成したuiをどうやってもそのまま使うのは不可能なので、ある程度、初発のアイデアを提供してくれるものとしてはよいんじゃないでしょうか。回数制限があるのでchatgptなどと組み合わせてなんとか回数制限を回避しながら使うとよろしいのではないかと思ったり。
Discussion