はじめに
皆さんは、個人開発って好きですか?
個人開発をやってみると、時間が足りなかったり、モチベーションが続かなかったりして、なかなか継続するのが難しいですよね。
私自身、「もっと簡単にアプリが作れたらなあ...」と、よく感じています。
そんな中、Googleが5月20日(火)に発表した新AIツール「Stitch」を使ってみたところ、UIデザインの作成時間を大幅に短縮し、サクッと個人開発に取り組めました!
この記事では、その体験をシェアしたいと思います。
「そもそもStitchって何?」という方は、以下の記事で詳しく解説しているので、ぜひチェックしてみてください!
今回作成するアプリは、シンプルなToDoアプリ「TaskMaster」です。
誰でも簡単に個人開発に挑戦できるよう、手順に沿って進めるだけで完成する内容になっています!
ぜひ、皆さんの環境でも一緒に作ってみてください。
この記事の目的は「個人開発の楽しさ」を体験してもらうことなので、Laravelの詳細な解説は省略します。
なお、Laravelを使って開発を進めていきますので、事前にLaravelプロジェクトを作成してください。
まだLaravelのローカル開発環境をお持ちでない方は、以下の記事を参考にセットアップをお願いします。
それでは、始めましょう!
ビューの作成
まずは、ユーザーに見せる画面部分であるビューを作成します。
さっそくStitchにアクセスして、ToDoアプリのUIデザインを作ってみましょう!
Webアプリケーションを作成するので、「Web」を選択し、「Make me a ToDo app.」とだけ入力して、デザインを生成してもらいます。
やることはこれだけです。
20秒ほど待つと、ToDoアプリのタスク一覧画面、タスク追加・編集画面、タスク詳細画面が、このように自動生成されました!
いい感じですね!
では、Stitchが生成したUIデザインのコードを、ビュー用のファイルに貼り付けましょう。
以下のコマンドでファイルを作成してください。
mkdir resources/views/layouts && touch resources/views/layouts/app.blade.php
mkdir resources/views/tasks && touch resources/views/tasks/{create,edit,index,show}.blade.php
作成できたら、以下の折りたたみセクションを開いて、それぞれのファイルにコードを貼り付けてください。
.blade.php
ファイルに貼り付けるため、生成されたコードの一部をLaravel Blade向けに調整しています。
resources\views\layouts\app.blade.php
<html>
<head>
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin="" />
<link
rel="stylesheet"
as="style"
onload="this.rel='stylesheet'"
href="https://fonts.googleapis.com/css2?display=swap&family=Inter%3Awght%40400%3B500%3B700%3B900&family=Noto+Sans%3Awght%40400%3B500%3B700%3B900"
/>
<title>Stitch Design</title>
<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64," />
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
</head>
<body>
<div class="relative flex size-full min-h-screen flex-col bg-white group/design-root overflow-x-hidden" style='font-family: Inter, "Noto Sans", sans-serif;'>
<div class="layout-container flex h-full grow flex-col">
<header class="flex items-center justify-between whitespace-nowrap border-b border-solid border-b-[#f0f2f5] px-10 py-3">
<div class="flex items-center gap-4 text-[#111418]">
<div class="size-4">
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 6H42L36 24L42 42H6L12 24L6 6Z" fill="currentColor"></path></svg>
</div>
<h2 class="text-[#111418] text-lg font-bold leading-tight tracking-[-0.015em]">TaskMaster</h2>
</div>
<div class="flex flex-1 justify-end gap-8">
<a href="{{ route('tasks.index') }}" class="flex max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 bg-[#f0f2f5] text-[#111418] gap-2 text-sm font-bold leading-normal tracking-[0.015em] min-w-0 px-2.5">
<div class="text-[#111418]" data-icon="ListBullets" data-size="20px" data-weight="regular">
<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" fill="currentColor" viewBox="0 0 256 256">
<path
d="M80,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H88A8,8,0,0,1,80,64Zm136,56H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM44,52A12,12,0,1,0,56,64,12,12,0,0,0,44,52Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,116Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,180Z"
></path>
</svg>
</div>
</a>
<div
class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10"
style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCg_qeL1_wnaNazznUbHCt6jqnmY_Tjtv0YoWu9OpTPfQlLmYMiOuKFDisAsnjRLkvoy9QI8ZhxE6EsvyLTZKHHg7S9WnYcjx20N1DrS1RpOCPR3O3sadstYTgSz68Iv3JdbKAMF1tmzKyi64EwV5IBQn3f2dzxf8qtEOXo4iFxpm6ss6xqiKLJjuROusBeVUrVblJU-2Ka3yRAnwApUxrdQnLiS3xRsd-T4fhTdeIvEtCFFSOHtIQUJYcbljjsZEA229tdZQq-J-I");'
></div>
</div>
</header>
<div class="px-40 flex flex-1 justify-center py-5">
@yield('content')
</div>
</div>
</div>
</body>
</html>
resources\views\tasks\create.blade.php
@extends('layouts.app')
@section('content')
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="flex flex-wrap justify-between gap-3 p-4"><p class="text-[#111418] tracking-light text-[32px] font-bold leading-tight min-w-72">Add Task</p></div>
<form action="{{ route('tasks.store') }}" method="POST">
@csrf
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<input
placeholder="Task name"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none h-14 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
value="{{ old('name') }}"
name="name"
required
/>
</label>
</div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<input
type="date"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none h-14 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
value="{{ old('due_date') }}"
name="due_date"
>
</label>
</div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<textarea
placeholder="Notes"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none min-h-36 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
name="note"
>{{ old('note') }}</textarea>
</label>
</div>
<div class="flex px-4 py-2">
<button
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-8 px-4 bg-[#3d98f4] text-white text-sm font-medium leading-normal"
>
<span class="truncate">Add Task</span>
</button>
</div>
</form>
</div>
@endsection
resources\views\tasks\edit.blade.php
@extends('layouts.app')
@section('content')
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="flex flex-wrap justify-between gap-3 p-4"><p class="text-[#111418] tracking-light text-[32px] font-bold leading-tight min-w-72">Edit Task</p></div>
<form action="{{ route('tasks.update', $task) }}" method="POST">
@csrf
@method('PATCH')
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<input
placeholder="Task name"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none h-14 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
value="{{ old('name', $task->name) }}"
name="name"
required
/>
</label>
</div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<input
type="date"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none h-14 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
value="{{ old('due_date', $task->due_date) }}"
name="due_date"
>
</label>
</div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<label class="flex flex-col min-w-40 flex-1">
<textarea
placeholder="Notes"
class="form-input flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-lg text-[#111418] focus:outline-0 focus:ring-0 border-none bg-[#f0f2f5] focus:border-none min-h-36 placeholder:text-[#60758a] p-4 text-base font-normal leading-normal"
name="note"
>{{ old('note', $task->note) }}</textarea>
</label>
</div>
<div class="flex px-4 py-2">
<button
class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-8 px-4 bg-[#3d98f4] text-white text-sm font-medium leading-normal"
>
<span class="truncate">Edit Task</span>
</button>
</div>
</form>
</div>
@endsection
resources\views\tasks\index.blade.php
@extends('layouts.app')
@section('content')
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="flex flex-wrap justify-between gap-3 p-4">
<p class="text-[#111418] tracking-light text-[32px] font-bold leading-tight min-w-72">My Tasks</p>
<a href="{{ route('tasks.create') }}" class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-8 px-4 bg-[#f0f2f5] text-[#111418] text-sm font-medium leading-normal truncate">New Task</a>
</div>
@foreach ($tasks as $task)
<div class="flex items-center gap-4 bg-white px-4 min-h-[72px] py-2 justify-between">
<div class="flex items-center gap-4">
<form action="{{ route('tasks.destroy', $task) }}" method="POST" class="m-0">
@csrf
@method('DELETE')
<button class="text-[#111418] flex items-center justify-center rounded-lg bg-[#f0f2f5] shrink-0 size-12" data-icon="Circle" data-size="24px" data-weight="regular">
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" fill="currentColor" viewBox="0 0 256 256">
<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z"></path>
</svg>
</button>
</form>
<div class="flex flex-col justify-center">
<p class="text-[#111418] text-base font-medium leading-normal line-clamp-1">{{ $task->name }}</p>
<p class="text-[#60758a] text-sm font-normal leading-normal line-clamp-2">{{ $task->due_date }}</p>
</div>
</div>
<div class="flex items-center gap-4">
<a href="{{ route('tasks.show', $task) }}" class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-8 px-4 bg-[#f0f2f5] text-[#111418] text-sm font-medium leading-normal truncate">Details</a>
<a href="{{ route('tasks.edit', $task) }}" class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-8 px-4 bg-[#f0f2f5] text-[#111418] text-sm font-medium leading-normal truncate">Edit</a>
</div>
</div>
@endforeach
</div>
@endsection
resources\views\tasks\show.blade.php
@extends('layouts.app')
@section('content')
<div class="layout-content-container flex flex-col max-w-[960px] flex-1">
<div class="flex flex-wrap justify-between gap-3 p-4"><p class="text-[#111418] tracking-light text-[32px] font-bold leading-tight min-w-72">{{ $task->name }}</p></div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<div class="flex flex-col min-w-40 flex-1">
<p class="text-[#111418] text-base font-medium leading-normal pb-2">Due Date</p>
<p class="flex w-full min-w-0 flex-1 overflow-hidden rounded-lg text-[#111418] border border-[#dbe0e6] bg-white h-14 p-[15px] text-base font-normal leading-normal">{{ $task->due_date }}</p>
</div>
</div>
<div class="flex max-w-[480px] flex-wrap items-end gap-4 px-4 py-2">
<div class="flex flex-col min-w-40 flex-1">
<p class="text-[#111418] text-base font-medium leading-normal pb-2">Notes</p>
<p class="flex w-full min-w-0 flex-1 overflow-hidden rounded-lg text-[#111418] border border-[#dbe0e6] bg-white h-14 p-[15px] text-base font-normal leading-normal whitespace-pre-line">{{ $task->note }}</p>
</div>
</div>
</div>
@endsection
貼り付けて保存したら、ビューの作成は完了です。
こんなに早くUIデザインが完成するのは、本当にありがたいですね!
モデルの作成
続いて、データベースの処理を担当するモデルを作成します。
以下のコマンドで、タスクの追加・取得・更新・削除を担当する Task
モデルと関連リソースを一括生成しましょう。
./vendor/bin/sail artisan make:model Task -mcr
今回のToDoアプリでは、タスクに「名前」「メモ」「期限」を設定できるようにしたいので、app/Models/Task.php
の Task
クラスに以下のコードを追記します。
protected $fillable = ['name', 'note', 'due_date'];
次に、マイグレーションファイル database/migrations/XXXX_XX_XX_XXXXXX_create_tasks_table.php
の Schema::create()
メソッドに、以下のコードを追記します。
マイグレーションファイルの XXXX_XX_XX_XXXXXX
には、作成日時が自動的に入ります。
$table->string('name');
$table->text('note')->nullable();
$table->date('due_date')->nullable();
以下のコマンドを実行すると、マイグレーションファイル(データベースの設計図)に基づいて、テーブルが作成されます。
./vendor/bin/sail artisan migrate
これでモデルの作成は完了です。
アプリの完成までもう一息ですね!
コントローラの作成
最後に、ユーザーのリクエストに応じてデータ処理や画面表示を指示するコントローラを作成します。
app/Http/Controllers/TaskController.php
の既存コードを削除し、以下のコードを貼り付けてください。
app/Http/Controllers/TaskController.php
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\Request;
class TaskController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$tasks = Task::all();
return view('tasks.index', compact('tasks'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('tasks.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
Task::create($request->all());
return redirect()->route('tasks.index');
}
/**
* Display the specified resource.
*/
public function show(Task $task)
{
return view('tasks.show', compact('task'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Task $task)
{
return view('tasks.edit', compact('task'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Task $task)
{
$task->update($request->all());
return redirect()->route('tasks.index');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Task $task)
{
$task->delete();
return redirect()->route('tasks.index');
}
}
次に、ウェブページのURLと処理を結びつけるため、routes\web.php
に以下のコードを追記します。
use App\Http\Controllers\TaskController;
Route::resource('/tasks', TaskController::class);
これでToDoアプリは完成です!
実際に触ってみる
それでは、以下のURLから完成したToDoアプリにアクセスしてみましょう!
ToDoアプリを作成した方のみアクセスできます。
同じ手順で開発した方は、アクセスすると以下のようなタスク一覧画面が表示されるはずです。
現在はタスクがないので、さっそく追加してみましょう!
「New Task」ボタンを押してタスク追加画面へ移動し、「企画プレゼン資料の作成」というタスクを追加してみます。
おお、ちゃんと追加されましたね!
先ほど追加したタスクに期限とメモを設定し忘れたので、「Edit」ボタンから編集してみましょう!
編集内容を入力したら「Edit Task」ボタンを押します。
無事に反映されましたね。
「Details」ボタンでタスクの詳細も確認しましたが、メモもきちんと追加されています!
最後に、複数のタスクを作成したので、タスク名の左にある「〇」ボタンからタスクを完了してみます。
「企画プレゼン資料の作成」を選択してみましたが、ちゃんと消えましたね!
1日でサクッと作ったアプリですが、思った以上にいい感じに仕上がりました...!
おわりに
今回は、Googleの新AIツール「Stitch」を使って、UIデザインからToDoアプリの開発までを体験してみました。
もちろん、多少の修正は必要ですが、UIデザインで悩まずに開発に集中できるというのは、大きなメリットだと感じました!
ぜひ皆さんも、プロトタイプの作成や個人開発を始めるきっかけとして、一度試してみてください!