v0.devとlaravel12 starter kitを組合せて何か作ってみる(プロジェクト管理アプリとか)

に公開

まずは雛形を作る

https://v0.dev/ にログイン。アカウントは必須なので適当に作成してください。以下のプロンプトでchatを開始。

非常に適当なプロンプトに合せて、ささっと作ってくれる。これをデプロイしたので見たい人はどうぞ https://v0-modern-project-dashboard-bhbjcu.vercel.app/

コンポーネントを別ファイルに分割指示

初回は全てのファイルがpage.tsxに集約されていて面倒だったのでこれを分割した

すると見た目の変化は無いものの以下のように分割してくれた

これをベースに移植していってみよう

starter kitのインストール

割愛。 詳しくは

https://zenn.dev/catatsumuri/articles/3789142e31bea4

からとか。

ともあれ、laravel new --reactとかで適当にセットアップ

認証画面は今回はカスタマイズしない


ダッシュボードは以下の通り

このplaceholderまみれのダッシュボードを改変する

レイアウト

今回v0が作ってきたレイアウトはトップにメニューがおかれているものであるがstarter kitのdefaultはサイドバー型式である。これは実は変更することもできる。

resources/js/layouts/app-layout.tsx
@@ -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 AppLayout from '@/layouts/app-layout';
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>
  );
}

タイトルと新規作成ボタン

あとはチマチマ移動していくだけ

resources/js/pages/dashboard.tsx
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部分

まあようするにこの部分であるが見た目がほぼ共通化しているのとバックエンドから送信するように改造してみてもいいだろう

routes/web.php
@@ -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');
 });

フロントエンド

resources/js/pages/dashboard.tsx
@@ -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