Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

OPENAI_API_KEY=
OPENAI_MODEL=
OPENAI_MODEL=

# Google OAuth (optional)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
81 changes: 81 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

IdeaBox is an open-source customer feedback and roadmap management tool built with Laravel 10, Inertia.js, React 18, and Tailwind CSS.

## Common Commands

```bash
# Development
yarn dev # Start Vite dev server with hot reload
php artisan serve # Start Laravel server at localhost:8000

# Build
yarn build # TypeScript check + Vite build + SSR build

# Testing
./vendor/bin/pest # Run all tests
./vendor/bin/pest tests/Feature/ExampleTest.php # Run single test file

# Code Formatting
./vendor/bin/pint # Format PHP code (Laravel Pint)

# Database
php artisan migrate # Run migrations
php artisan db:seed # Seed database (creates admin@example.com / password)
```

## Architecture

### Tech Stack
- **Backend**: Laravel 10 with Eloquent ORM
- **Frontend**: React 18 + TypeScript via Inertia.js (SSR enabled)
- **Styling**: Tailwind CSS with TailReact component library (@wedevs/tail-react)
- **Testing**: PestPHP

### Directory Structure
- `app/Http/Controllers/Admin/` - Admin panel controllers
- `app/Http/Controllers/Frontend/` - Public-facing controllers
- `app/Services/` - Business logic (OpenAIService, SettingService, integrations)
- `app/Jobs/` - Background jobs for notifications
- `resources/js/Pages/` - Inertia React page components
- `resources/js/Components/` - Reusable React components
- `resources/js/Layouts/` - Layout components (GuestLayout, FrontendLayout, AuthenticatedLayout)

### Core Models
- **Board** - Feedback categories/boards
- **Post** - Feedback items with votes, comments, status, ETA/impact/effort
- **Status** - Customizable status options for posts
- **IntegrationProvider/IntegrationRepository** - External service connections (GitHub)

### Integration System
New integrations extend `BaseIntegration` implementing `IntegrationInterface`, registered in `IntegrationServiceProvider`. See `docs/integrations.md` for details.

## Code Conventions

### PHP/Laravel
- Use `declare(strict_types=1);` in all PHP files
- Use Form Request classes for validation with array notation: `'email' => ['required', 'email']`
- Prefer service classes for business logic over fat controllers
- Use `Model::query()` instead of direct static methods
- Always eager load relations to avoid N+1 queries
- Define routes manually (no `Route::resource()`), always name routes
- Use tuple notation: `Route::get('about', [AboutController::class, 'index'])`

### React/TypeScript
- **Always use TailReact components** when available (Button, Modal, TextField, SelectInput, Table, Notice, Badge, etc.)
- Import from `@wedevs/tail-react` or `tail-react`
- Path alias: `@/*` maps to `resources/js/*`
- Inertia pages receive data as props from Laravel controllers

### Key TailReact Components
- `Button` - variant: primary/secondary/danger, style: fill/outline/link
- `Modal, ModalHeader, ModalBody, ModalActions` - Dialog components
- `ConfirmModal` - Simple yes/no confirmation
- `TextField`, `Textarea`, `SelectInput` - Form inputs
- `Table, TableHeader, TableBody` - Data tables
- `Notice` - Alerts (type: success/warning/error/info)
- `Badge` - Status indicators
10 changes: 10 additions & 0 deletions app/Http/Controllers/Auth/AuthenticatedSessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,19 @@ public function create(): Response
return Inertia::render('Auth/Login', [
'canResetPassword' => Route::has('password.request'),
'status' => session('status'),
'googleAuthEnabled' => $this->isGoogleAuthEnabled(),
]);
}

/**
* Check if Google OAuth is configured.
*/
private function isGoogleAuthEnabled(): bool
{
return !empty(config('services.google.client_id'))
&& !empty(config('services.google.client_secret'));
}

/**
* Handle an incoming authentication request.
*/
Expand Down
13 changes: 12 additions & 1 deletion app/Http/Controllers/Auth/RegisteredUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,18 @@ class RegisteredUserController extends Controller
*/
public function create(): Response
{
return Inertia::render('Auth/Register');
return Inertia::render('Auth/Register', [
'googleAuthEnabled' => $this->isGoogleAuthEnabled(),
]);
}

/**
* Check if Google OAuth is configured.
*/
private function isGoogleAuthEnabled(): bool
{
return !empty(config('services.google.client_id'))
&& !empty(config('services.google.client_secret'));
}

/**
Expand Down
82 changes: 82 additions & 0 deletions app/Http/Controllers/Auth/SocialLoginController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;

class SocialLoginController extends Controller
{
/**
* Redirect to Google OAuth.
*/
public function redirectToGoogle(): RedirectResponse
{
return Socialite::driver('google')->redirect();
}

/**
* Handle Google OAuth callback.
*/
public function handleGoogleCallback(): RedirectResponse
{
try {
$googleUser = Socialite::driver('google')->user();
} catch (\Exception $e) {
return redirect()->route('login')->with('error', 'Unable to authenticate with Google. Please try again.');
}

// Check if user exists with this Google ID
$user = User::query()->where('google_id', $googleUser->getId())->first();

if ($user) {
// Ensure email is verified for existing Google users
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
}

Auth::login($user, true);

return redirect()->intended('/');
}

// Check if user exists with this email
$user = User::query()->where('email', $googleUser->getEmail())->first();

if ($user) {
// Link Google account to existing user and mark email as verified
$user->update([
'google_id' => $googleUser->getId(),
'avatar_url' => $googleUser->getAvatar(),
'email_verified_at' => $user->email_verified_at ?? now(),
]);

Auth::login($user, true);

return redirect()->intended('/');
}

// Create new user with email already verified (Google verified it)
// The Registered event listener checks hasVerifiedEmail() before sending
$user = User::query()->create([
'name' => $googleUser->getName(),
'email' => $googleUser->getEmail(),
'google_id' => $googleUser->getId(),
'avatar_url' => $googleUser->getAvatar(),
'email_verified_at' => now(),
'role' => User::ROLE_USER,
]);

event(new Registered($user));

Auth::login($user, true);

return redirect()->intended('/');
}
}
7 changes: 7 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class User extends Authenticatable implements MustVerifyEmail
'password',
'role',
'email_preferences',
'google_id',
'avatar_url',
];

/**
Expand All @@ -42,6 +44,7 @@ class User extends Authenticatable implements MustVerifyEmail
'email_verified_at',
'created_at',
'updated_at',
'google_id',
];

/**
Expand Down Expand Up @@ -85,6 +88,10 @@ protected static function boot()
*/
public function getAvatarAttribute(): string
{
if ($this->avatar_url) {
return $this->avatar_url;
}

return 'https://www.gravatar.com/avatar/' . md5($this->email) . '?s=56&d=mm';
}

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"inertiajs/inertia-laravel": "^0.6.3",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.8",
"openai-php/client": "^0.10.3",
"predis/predis": "^2.3",
Expand Down
Loading