diff --git a/.github/skills/tailwindcss-development/SKILL.md b/.github/skills/tailwindcss-development/SKILL.md index 7c8e295..d2802e6 100644 --- a/.github/skills/tailwindcss-development/SKILL.md +++ b/.github/skills/tailwindcss-development/SKILL.md @@ -10,7 +10,7 @@ metadata: ## Documentation -Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. +Use `search-docs` for detailed Tailwind CSS v3 patterns and documentation. ## Basic Usage @@ -18,55 +18,22 @@ Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. - Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue). - Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically. -## Tailwind CSS v4 Specifics +## Tailwind CSS v3 Specifics -- Always use Tailwind CSS v4 and avoid deprecated utilities. -- `corePlugins` is not supported in Tailwind v4. +- Always use Tailwind CSS v3 and verify you're using only classes it supports. +- Configuration is done in the `tailwind.config.js` file. +- Import using `@tailwind` directives: -### CSS-First Configuration - -In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed: - - + ```css -@theme { - --color-brand: oklch(0.72 0.11 178); -} +@tailwind base; +@tailwind components; +@tailwind utilities; ``` -### Import Syntax - -In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3: - - -```diff -- @tailwind base; -- @tailwind components; -- @tailwind utilities; -+ @import "tailwindcss"; -``` - -### Replaced Utilities - -Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric. - -| Deprecated | Replacement | -|------------|-------------| -| bg-opacity-* | bg-black/* | -| text-opacity-* | text-black/* | -| border-opacity-* | border-black/* | -| divide-opacity-* | divide-black/* | -| ring-opacity-* | ring-black/* | -| placeholder-opacity-* | placeholder-black/* | -| flex-shrink-* | shrink-* | -| flex-grow-* | grow-* | -| overflow-ellipsis | text-ellipsis | -| decoration-slice | box-decoration-slice | -| decoration-clone | box-decoration-clone | - ## Spacing -Use `gap` utilities instead of margins for spacing between siblings: +When listing items, use gap utilities for spacing; don't use margins. ```html @@ -110,10 +77,15 @@ If existing pages and components support dark mode, new pages and components mus ``` +## Verification + +1. Check browser for visual rendering +2. Test responsive breakpoints +3. Verify dark mode if project uses it + ## Common Pitfalls -- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.) -- Using `@tailwind` directives instead of `@import "tailwindcss"` -- Trying to use `tailwind.config.js` instead of CSS `@theme` directive - Using margins for spacing between siblings instead of gap utilities -- Forgetting to add dark mode variants when the project uses dark mode \ No newline at end of file +- Forgetting to add dark mode variants when the project uses dark mode +- Not checking existing project conventions before adding new utilities +- Overusing inline styles when Tailwind classes would suffice \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 39b748c..d6ba8d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,13 +109,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. -=== tests rules === - -# Test Enforcement - -- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. -- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. - === laravel/core rules === # Do Things the Laravel Way diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php new file mode 100644 index 0000000..ef52efe --- /dev/null +++ b/app/Http/Controllers/CategoryController.php @@ -0,0 +1,80 @@ +latest() + ->paginate(10, ['id', 'name', 'slug', 'description', 'color', 'created_at']); + + return view('category.index', [ + 'categories' => $categories, + ]); + } + + /** + * Show the form for creating a new category. + */ + public function create(): View + { + return view('category.create'); + } + + /** + * Store a newly created category. + */ + public function store(StoreCategoryRequest $request): RedirectResponse + { + Category::query()->create($request->validated()); + + return redirect() + ->route('category.index') + ->with('status', 'category-created'); + } + + /** + * Show the form for editing the specified category. + */ + public function edit(Category $category): View + { + return view('category.edit', [ + 'category' => $category, + ]); + } + + /** + * Update the specified category. + */ + public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse + { + $category->update($request->validated()); + + return redirect() + ->route('category.edit', $category) + ->with('status', 'category-updated'); + } + + /** + * Remove the specified category. + */ + public function destroy(Category $category): RedirectResponse + { + $category->delete(); + + return redirect() + ->route('category.index') + ->with('status', 'category-deleted'); + } +} diff --git a/app/Http/Requests/StoreCategoryRequest.php b/app/Http/Requests/StoreCategoryRequest.php new file mode 100644 index 0000000..d40521f --- /dev/null +++ b/app/Http/Requests/StoreCategoryRequest.php @@ -0,0 +1,43 @@ +merge([ + 'slug' => Str::slug((string) ($this->filled('slug') ? $this->input('slug') : $this->input('name'))), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255', 'unique:categories,name'], + 'slug' => ['required', 'string', 'max:255', 'alpha_dash:ascii', 'unique:categories,slug'], + 'description' => ['nullable', 'string', 'max:1000'], + 'color' => ['required', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'], + ]; + } +} diff --git a/app/Http/Requests/UpdateCategoryRequest.php b/app/Http/Requests/UpdateCategoryRequest.php new file mode 100644 index 0000000..a14b143 --- /dev/null +++ b/app/Http/Requests/UpdateCategoryRequest.php @@ -0,0 +1,46 @@ +merge([ + 'slug' => Str::slug((string) ($this->filled('slug') ? $this->input('slug') : $this->input('name'))), + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + $categoryId = $this->route('category')?->id; + + return [ + 'name' => ['required', 'string', 'max:255', Rule::unique('categories', 'name')->ignore($categoryId)], + 'slug' => ['required', 'string', 'max:255', 'alpha_dash:ascii', Rule::unique('categories', 'slug')->ignore($categoryId)], + 'description' => ['nullable', 'string', 'max:1000'], + 'color' => ['required', 'string', 'regex:/^#[0-9A-Fa-f]{6}$/'], + ]; + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..c2a45a2 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,15 @@ + */ + use HasFactory; +} diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 0000000..0dc23f3 --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,30 @@ + + */ +class CategoryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $name = fake()->unique()->words(2, true); + + return [ + 'name' => Str::title($name), + 'slug' => Str::slug($name), + 'description' => fake()->sentence(), + 'color' => fake()->hexColor(), + ]; + } +} diff --git a/database/migrations/2026_05_12_022545_create_categories_table.php b/database/migrations/2026_05_12_022545_create_categories_table.php new file mode 100644 index 0000000..5055570 --- /dev/null +++ b/database/migrations/2026_05_12_022545_create_categories_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->string('color', 7)->default('#4f46e5'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php new file mode 100644 index 0000000..3a6017f --- /dev/null +++ b/database/seeders/CategorySeeder.php @@ -0,0 +1,29 @@ + 'Product Updates', 'slug' => 'product-updates', 'description' => 'Announcements and release notes.', 'color' => '#4f46e5'], + ['name' => 'Operations', 'slug' => 'operations', 'description' => 'Internal workflow and process items.', 'color' => '#10b981'], + ['name' => 'Marketing', 'slug' => 'marketing', 'description' => 'Campaigns, content, and promotions.', 'color' => '#f59e0b'], + ['name' => 'Support', 'slug' => 'support', 'description' => 'Customer support and service categories.', 'color' => '#ec4899'], + ]; + + foreach ($categories as $category) { + Category::query()->updateOrCreate( + ['slug' => $category['slug']], + $category, + ); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..3d6e3ff 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,6 +17,8 @@ class DatabaseSeeder extends Seeder { // User::factory(10)->create(); + $this->call(CategorySeeder::class); + User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', diff --git a/package-lock.json b/package-lock.json index 073163b..be187f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,6 @@ { "name": "git-amarul", + "name": "git-iszuddin", "lockfileVersion": 3, "requires": true, "packages": { @@ -32,29 +33,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -964,6 +942,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2153,6 +2132,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2200,6 +2180,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2629,6 +2610,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -2806,6 +2788,7 @@ "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/resources/views/category/create.blade.php b/resources/views/category/create.blade.php new file mode 100644 index 0000000..238d283 --- /dev/null +++ b/resources/views/category/create.blade.php @@ -0,0 +1,63 @@ + + +
+

{{ __('Categories') }}

+

+ {{ __('Create Category') }} +

+
+
+ +
+
+
+
+

{{ __('Category Details') }}

+

{{ __('Choose a clear name and a color that stands out in lists.') }}

+
+ +
+ @csrf + +
+
+ + + +
+ +
+ + + +
+
+ +
+ + + +
+ +
+ +
+ + {{ __('Select a category accent color.') }} +
+ +
+ +
+ + {{ __('Cancel') }} + + +
+
+
+
+
+
diff --git a/resources/views/category/edit.blade.php b/resources/views/category/edit.blade.php new file mode 100644 index 0000000..22ee77c --- /dev/null +++ b/resources/views/category/edit.blade.php @@ -0,0 +1,92 @@ + + +
+
+

{{ __('Categories') }}

+

+ {{ __('Edit Category') }} +

+
+ + + {{ __('Back to Categories') }} + +
+
+ +
+
+ @if (session('status') === 'category-updated') +
+ {{ __('Category updated successfully.') }} +
+ @endif + +
+
+
+ +
+

{{ $category->name }}

+

{{ $category->slug }}

+
+
+
+ +
+ @csrf + @method('DELETE') +
+ +
+ @csrf + @method('PATCH') + +
+
+ + + +
+ +
+ + + +
+
+ +
+ + + +
+ +
+ +
+ + {{ __('Adjust the accent used across category lists.') }} +
+ +
+ +
+ + +
+ + {{ __('Cancel') }} + + +
+
+
+
+
+
+
diff --git a/resources/views/category/index.blade.php b/resources/views/category/index.blade.php new file mode 100644 index 0000000..5657a1b --- /dev/null +++ b/resources/views/category/index.blade.php @@ -0,0 +1,126 @@ + + +
+
+

{{ __('Content Library') }}

+

+ {{ __('Categories') }} +

+
+ + + {{ __('New Category') }} + +
+
+ +
+
+ @if (session('status')) +
+ @if (session('status') === 'category-created') + {{ __('Category created successfully.') }} + @elseif (session('status') === 'category-updated') + {{ __('Category updated successfully.') }} + @elseif (session('status') === 'category-deleted') + {{ __('Category deleted successfully.') }} + @endif +
+ @endif + +
+
+
+
+
+

{{ __('Manage Categories') }}

+

{{ __('Organize records with clear names, slugs, and color labels.') }}

+
+ + {{ trans_choice(':count category|:count categories', $categories->total(), ['count' => $categories->total()]) }} + +
+
+ +
+ + + + + + + + + + + @forelse ($categories as $category) + + + + + + + @empty + + + + @endforelse + +
{{ __('Category') }}{{ __('Slug') }}{{ __('Created') }}{{ __('Actions') }}
+
+ +
+

{{ $category->name }}

+

+ {{ $category->description ?: __('No description added.') }} +

+
+
+
+ {{ $category->slug }} + {{ $category->created_at?->format('Y-m-d H:i') }} +
+ + {{ __('Edit') }} + +
+ @csrf + @method('DELETE') + + +
+
+
+
+
+

{{ __('No categories yet') }}

+

{{ __('Create your first category to start organizing the system.') }}

+ + {{ __('Create Category') }} + +
+
+
+ + @if ($categories->hasPages()) +
+ {{ $categories->links() }} +
+ @endif +
+ + +
+
+
+
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 0000000..5bacba7 --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,126 @@ + + + + + + + Neighborhood News Portal + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ @if (\Illuminate\Support\Facades\Route::has('login')) +
+ +
+ @endif + +
+ Forested hills in the distance +
+
+
+

Local Neighborhood Portal

+
+
+

Taman Melawati News

+

+ Friendly updates from around the block. Stories are placeholders for now, but the spirit is real. +

+

Inspired by the hills and forest edges around our neighborhood

+
+
+

Good morning, Neighbor

+

Tuesday community digest

+
+
+
+
+ +
+
+
+ Misty forested mountain backdrop +
+
+

Top Story

+

Community Garden Harvest Day Set for Saturday

+

+ Volunteers from Cedar Lane and Oak Street are gathering at 8:00 AM to pick tomatoes, herbs, and okra. + Bring a reusable bag and a smile. Extra produce will be shared with nearby families. +

+
+ By Hana, local editor + 2 hours ago +
+
+
+ +
+
+

Street Update

+

Pine Street Lighting Repaired

+

Evening walks are brighter again after three new lamps were installed this week.

+
+
+

School Corner

+

Book Drive Reaches 500 Donations

+

Taman Melawati Elementary thanks neighbors for donating books to the weekend reading club.

+
+
+
+ + +
+
+ + \ No newline at end of file diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 9e9e0a1..f571c10 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -21,6 +21,9 @@ {{ __('Roles') }} + + {{ __('Categories') }} + @@ -82,6 +85,9 @@ {{ __('Roles') }} + + {{ __('Categories') }} + diff --git a/routes/web.php b/routes/web.php index 5fd84f5..e2e8c08 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,13 +1,14 @@ group(function () { Route::get('/role', [RoleController::class, 'index'])->name('role.index'); Route::get('/role/create', [RoleController::class, 'create'])->name('role.create'); Route::post('/role', [RoleController::class, 'store'])->name('role.store'); + Route::resource('category', CategoryController::class)->except('show'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); diff --git a/tests/Feature/CategoryCrudTest.php b/tests/Feature/CategoryCrudTest.php new file mode 100644 index 0000000..699ba04 --- /dev/null +++ b/tests/Feature/CategoryCrudTest.php @@ -0,0 +1,96 @@ +create(); + + $this->get('/category')->assertRedirect('/login'); + $this->get("/category/{$category->id}/edit")->assertRedirect('/login'); +}); + +test('authenticated users can view categories', function () { + Category::factory()->create([ + 'name' => 'Design', + 'slug' => 'design', + 'description' => 'Visual work', + 'color' => '#ec4899', + ]); + + $response = $this + ->actingAs(User::factory()->create()) + ->get('/category'); + + $response + ->assertSuccessful() + ->assertSee('Categories') + ->assertSee('Design') + ->assertSee('design') + ->assertSee('Visual work'); +}); + +test('authenticated users can create a category', function () { + $response = $this + ->actingAs(User::factory()->create()) + ->post('/category', [ + 'name' => 'Operations', + 'slug' => '', + 'description' => 'Internal workflows', + 'color' => '#10b981', + ]); + + $response->assertRedirect('/category'); + + $this->assertDatabaseHas('categories', [ + 'name' => 'Operations', + 'slug' => 'operations', + 'description' => 'Internal workflows', + 'color' => '#10b981', + ]); +}); + +test('authenticated users can update a category', function () { + $category = Category::factory()->create([ + 'name' => 'Legacy', + 'slug' => 'legacy', + 'color' => '#4f46e5', + ]); + + $response = $this + ->actingAs(User::factory()->create()) + ->patch("/category/{$category->id}", [ + 'name' => 'Marketing', + 'slug' => 'marketing', + 'description' => 'Campaign planning', + 'color' => '#f59e0b', + ]); + + $response->assertRedirect("/category/{$category->id}/edit"); + + $this->assertDatabaseHas('categories', [ + 'id' => $category->id, + 'name' => 'Marketing', + 'slug' => 'marketing', + 'description' => 'Campaign planning', + 'color' => '#f59e0b', + ]); +}); + +test('authenticated users can delete a category', function () { + $category = Category::factory()->create([ + 'name' => 'Archive', + 'slug' => 'archive', + 'color' => '#64748b', + ]); + + $response = $this + ->actingAs(User::factory()->create()) + ->delete("/category/{$category->id}"); + + $response->assertRedirect('/category'); + + $this->assertDatabaseMissing('categories', [ + 'id' => $category->id, + ]); +}); diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8b5843f..c86615d 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -3,5 +3,10 @@ it('returns a successful response', function () { $response = $this->get('/'); - $response->assertStatus(200); + $response + ->assertSuccessful() + ->assertSee('Taman Melawati News') + ->assertSee('Community Garden Harvest Day Set for Saturday') + ->assertSee('Login') + ->assertSee('Register'); });