Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7a8dd3a
Resolve merge conflicts
mohanad69 Feb 9, 2026
3995a07
review site pages
mohanad69 Feb 10, 2026
8acc4cf
fix courses issues
mohanad69 Feb 15, 2026
e31230b
dashboard, profile, dark mode, password change
mohanad69 Feb 18, 2026
b4e445e
Refactor AppSidebar and AppLayout components, adding new navigation i…
aemaddin Feb 18, 2026
22d73a0
fixing sidebar, my profile, glossary, my books, my courses
mohanad69 Feb 23, 2026
df9fcd7
fixing sidebar, my profile, glossary, my books, my courses
mohanad69 Feb 23, 2026
9e06a36
fixing
mohanad69 Mar 2, 2026
eeb1191
refactor cobtroller
mohanad69 Mar 3, 2026
6af3907
book controller refactor
mohanad69 Mar 3, 2026
7cee2b5
kashier config, book controller policy fix, faqs head, course section…
mohanad69 Mar 4, 2026
8a27630
fix download book path issue
mohanad69 Mar 4, 2026
99f924a
book policy remove db logic, fix send direct link download, fix order…
mohanad69 Mar 5, 2026
b6ec83e
email verification, send direct download link, translation issues in …
mohanad69 Mar 8, 2026
af656fc
fix nav menu, kashier test, my orders, remove vendor fees, make href …
mohanad69 Mar 9, 2026
e4726aa
fixing
mohanad69 Mar 10, 2026
9b15e15
fixing issues in github
mohanad69 Mar 11, 2026
3bd8eb1
country dropdown phone
mohanad69 Mar 12, 2026
4e829bf
alaphet error fix in glossarues, start edit email templates design
mohanad69 Mar 15, 2026
0a952a2
registered users fix, edit profile phone code, send book link email
mohanad69 Mar 17, 2026
9e30af7
make config file for site settings, aabic input names validation tran…
mohanad69 Mar 24, 2026
b6d9849
translation manager
mohanad69 Mar 25, 2026
654f68e
send download test
mohanad69 Mar 26, 2026
018d4ba
fixing course progress
mohanad69 Mar 29, 2026
a3b0398
fix slides, save video progress
mohanad69 Mar 30, 2026
5104337
fixing responsive style
mohanad69 Apr 1, 2026
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
32 changes: 32 additions & 0 deletions app/Actions/Auth/CreateZohoLeadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Actions\Auth;

use Asciisd\ZohoV8\Models\ZohoLead;
use Illuminate\Support\Facades\Log;

class CreateZohoLeadAction
{
/**
* Create a Zoho lead
*/
public function execute(array $data): mixed
{
try {
return ZohoLead::create([
'First_Name' => $data['first_name'],
'Last_Name' => $data['last_name'],
'Email' => $data['email'],
'Phone' => $data['phone'],
'Lead_Source' => $data['lead_source'],
]);
} catch (\Throwable $e) {
Log::channel('zoho_sync')->error('ZohoLead creation failed', [
'email' => $data['email'] ?? null,
'error' => $e->getMessage(),
]);

return null;
}
}
}
62 changes: 62 additions & 0 deletions app/Actions/Books/GenerateBookDownloadUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace App\Actions\Books;

use App\Models\Book;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;

class GenerateBookDownloadUrl
{
/**
* Generate a temporary download URL for a book
*/
public function execute(Book $book, int $minutesValid = 30): ?string
{
try {
// Check if file_src exists
if (!$book->file_src) {
Log::warning('Book file_src is empty', ['book_id' => $book->id]);
return null;
}

$path = $this->extractPath($book->file_src);

if (!$path) {
Log::warning('Could not extract path from file_src', [
'book_id' => $book->id,
'file_src' => $book->file_src
]);
return null;
}

return Storage::disk('s3_public')->temporaryUrl(
$path,
now()->addMinutes($minutesValid)
);

} catch (\Exception $e) {
Log::error('Failed to generate book download URL', [
'book_id' => $book->id,
'error' => $e->getMessage()
]);
return null;
}
}

/**
* Extract path from file_src (supports both JSON and plain string)
*/
private function extractPath(string $fileSrc): ?string
{
// Try to decode as JSON first
$decoded = json_decode($fileSrc);

if (json_last_error() === JSON_ERROR_NONE && is_object($decoded) && isset($decoded->path)) {
return $decoded->path;
}

// If not JSON, treat as plain path
return trim($fileSrc) ?: null;
}
}
78 changes: 17 additions & 61 deletions app/Http/Controllers/Auth/RegisteredUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterUserRequest;
use App\Models\User;
use App\Services\UserRegistrationService;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Inertia\Inertia;
use Inertia\Response;
use Asciisd\ZohoV8\Models\ZohoLead;
use Illuminate\Support\Facades\Log;

class RegisteredUserController extends Controller
{
public function __construct(
private UserRegistrationService $registrationService
) {}

/**
* Show the registration page.
*/
Expand All @@ -30,71 +32,25 @@ public function create(): Response
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
public function store(RegisterUserRequest $request): RedirectResponse
{
$request->validate([
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
'phone' => 'required||phone|max:255|unique:'.User::class,
'password' => ['required', 'confirmed', Rules\Password::defaults()],
// 'country' => 'required|string|max:2',
// 'terms' => 'required|accepted',
]);
$validated = $request->validated();

$utm_data = null;
if (request()->hasCookie('utm')) {
$utm_data = json_decode(request()->cookie('utm'));
}
// Extract UTM data
$utmData = $this->registrationService->extractUtmData();

$user = User::create([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $request->email,
'phone' => $request->phone,
'country' => $request->country,
'password' => Hash::make($request->password),
'utm_source' => $utm_data->utm_source ?? null,
'utm_content' => $utm_data->utm_content ?? null,
'utm_medium' => $utm_data->utm_medium ?? null,
'utm_campaign' => $utm_data->utm_campaign ?? null,
'utm_term' => $utm_data->utm_term ?? null,
]);
// Register user
$user = $this->registrationService->register($validated, $utmData);

// Create Zoho Contact
try {
$this->createZohoLead([
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
'phone' => $user->phone,
'lead_source' => 'trader_factory',
]);
} catch (\Throwable $e) {
Log::channel('zoho_sync')->error('ZohoContact creation failed', [
'user_id' => $user->id,
'email' => $user->email,
'error' => $e->getMessage(),
]);
}
// Create Zoho Lead (non-blocking)
$this->registrationService->createZohoLead($user);

// Fire registered event
event(new Registered($user));

// Login user
Auth::login($user);

return to_route('dashboard');
}

public function createZohoLead(array $data)
{
return ZohoLead::create([
'First_Name' => $data['first_name'],
'Last_Name' => $data['last_name'],
'Email' => $data['email'],
'Phone' => $data['phone'],
'Lead_Source' => $data['lead_source'],
]);
}


}
}
81 changes: 34 additions & 47 deletions app/Http/Controllers/BookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,85 +2,72 @@

namespace App\Http\Controllers;

use App\Actions\Books\GenerateBookDownloadUrl;
use App\Http\Resources\BookResource;
use App\Models\Book;
use App\Models\Order;
use App\Notifications\BookInvoice;
use App\Notifications\SendBookLink;
use Illuminate\Support\Facades\Storage;
use App\Services\BookService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

class BookController extends Controller
{
public function index()
use AuthorizesRequests;

public function __construct(
private BookService $bookService,
private GenerateBookDownloadUrl $generateDownloadUrl
) {}

public function index(): Response
{
return Inertia::render('books/Index', [
'books' => BookResource::collection(Book::all())
])->with('success', 'تم إرسال رابط التحميل على البريد الإلكتروني');
'books' => BookResource::collection(Book::all()),
]);
}

/**
* Send download link to user's email
*/
public function sendDownloadLink(Book $book)
public function sendDownloadLink(Book $book): RedirectResponse
{
$user = auth()->user();
$user->notify(new SendBookLink($book));
$this->authorize('download', $book);
$this->bookService->sendDownloadLink($book, auth()->user());

return redirect()->back()->with('success', 'تم إرسال رابط التحميل على البريد الإلكتروني');
// return redirect()->back()->with('success', 'تم إرسال رابط التحميل على البريد الإلكتروني');
return redirect()->route('books.download.page', $book->id)
->with('success', __('locale.download_link_sent'));
}

/**
* Display download page for a book
*/
public function downloadPage(Book $book)
public function downloadPage(Book $book): Response|RedirectResponse
{
// Check if the user owns the book
if (! $book->purchased()) {
return redirect()->route('books.index')->with('error', 'لا يمكنك تحميل هذا الكتاب لأنك لم تشتريه بعد');
}

// Get the latest order for this book
$order = Order::where([
'user_id' => auth()->id(),
'orderable_type' => Book::class,
'orderable_id' => $book->id,
'status' => 'SUCCESS',
])->latest()->first();
$this->authorize('download', $book);

// Send invoice email if it's a new purchase
if ($order && $order->created_at->diffInMinutes(now()) < 5) {
auth()->user()->notify(new BookInvoice($book, $order));
}

// Generate temporary download URL
$url = null;
if ($book->file_src) {
$path = json_decode($book->file_src)->path;
$url = Storage::disk('s3_public')->temporaryUrl($path, now()->addMinutes(30));
}
$data = $this->bookService->prepareDownloadData($book, auth()->user());

return Inertia::render('Books/Download', [
'book' => BookResource::make($book),
'downloadUrl' => $url,
return Inertia::render('books/Download', [
'book' => BookResource::make($data['book']),
'downloadUrl' => $data['downloadUrl'],
]);
}

/**
* Direct download of the book file
*/
public function directDownload(Book $book)
public function directDownload(Book $book): RedirectResponse
{
// Check if the user owns the book
if (! $book->purchased()) {
return redirect()->route('books.index')->with('error', 'لا يمكنك تحميل هذا الكتاب لأنك لم تشتريه بعد');
}
$this->authorize('download', $book);

$url = $this->generateDownloadUrl->execute($book, 5);

if (! $book->file_src) {
if (! $url) {
return redirect()->back()->with('error', 'ملف الكتاب غير متوفر');
}

$path = json_decode($book->file_src)->path;

return redirect(Storage::disk('s3_public')->temporaryUrl($path, now()->addMinutes(5)));
return redirect($url);
}
}
11 changes: 9 additions & 2 deletions app/Http/Controllers/CourseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ class CourseController extends Controller
public function index(Course $course): InertiaResponse
{
return Inertia::render('courses/Index', [
'courses' => CourseResource::collection(Course::with('sections.lessons')->get()),
'courses' => CourseResource::collection(
Course::query()
->isPublished()
->isActive()
->hasSections()
->with('sections.lessons')
->get()
),
'metadata' => MetaData::where('page_slug', 'courses')->first(),
]);
}
Expand Down Expand Up @@ -49,4 +56,4 @@ public function sections(Section $section): InertiaResponse
'metadata' => MetaData::where('page_slug', $section->slug)->first(),
]);
}
}
}
8 changes: 7 additions & 1 deletion app/Http/Controllers/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers;

use App\Enums\OrderStatus;
use App\Http\Resources\OrderResource;
use App\Models\Order;
use Inertia\Inertia;
Expand All @@ -10,10 +11,15 @@ class DashboardController extends Controller
{
public function __invoke()
{
$user = auth()->user()->loadCount(['courseOrders', 'bookOrders', 'orders']);

return Inertia::render('Dashboard', [
'orders' => OrderResource::collection(
Order::latest()->with('orderable')->get()
Order::where('status', OrderStatus::Success)->latest()->with('orderable')->get()
),
'courses_count' => $user->course_orders_count,
'books_count' => $user->book_orders_count,
'orders_count' => $user->orders_count,
]);
}
}
Loading
Loading