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
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Make sure a user can't favorite himself.

> Please update the following line in this `README.md` file to include your estimate of the time required for completion.

Estimated Time Required: [Your Estimate Here]
Estimated Time Required: 2 hours

> After updating the estimate and right before you start coding, commit your changes using the following command:
`git add README.md && git commit -m "Task 1 estimated" && git push`
Expand Down Expand Up @@ -110,7 +110,7 @@ As in the previous task, please make sure to add the relevant tests in `tests/Fe

> Please update the following line in this `README.md` file to include your estimate of the time required for completion.

Estimated Time Required: [Your Estimate Here]
Estimated Time Required: 1 hour

> After updating the estimate and right before you start coding, commit your changes using the following command:
`git add README.md && git commit -m "Task 2 estimated" && git push`
Expand Down Expand Up @@ -138,7 +138,7 @@ In addition to the implementation, please ensure comprehensive test coverage for

> Please update the following line in this `README.md` file to include your estimate of the time required for completion.

Estimated Time Required: [Your Estimate Here]
Estimated Time Required: 35 minutes

> After updating the estimate and right before you start coding, commit your changes using the following command:
`git add README.md && git commit -m "Task 3 estimated" && git push`
Expand All @@ -162,7 +162,7 @@ Ensure that the corresponsing tests are provided for this feature.

> Please update the following line in this `README.md` file to include your estimate of the time required for completion.

Estimated Time Required: [Your Estimate Here]
Estimated Time Required: 35 minutes

> After updating the estimate and right before you start coding, commit your changes using the following command:
`git add README.md && git commit -m "Task 4 estimated" && git push`
Expand All @@ -184,7 +184,7 @@ Once an image is attached, the endpoint should save it appropriately. The URL of

> Please update the following line in this `README.md` file to include your estimate of the time required for completion.

Estimated Time Required: [Your Estimate Here]
Estimated Time Required: 30 minutes

> After updating the estimate and right before you start coding, commit your changes using the following command:
`git add README.md && git commit -m "Extra Task estimated" && git push`
Expand All @@ -199,5 +199,3 @@ Estimated Time Required: [Your Estimate Here]
Congratulations! You completed the first part of this interview coding challenge. We'll now leave the API behind and enter the front-end realm.

Please go to the front-end codebase and follow the instructions you'll find on the `README`. Thank you and good luck!


128 changes: 128 additions & 0 deletions app/Console/Commands/ImportUsersFromApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

class ImportUsersFromApi extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:import-from-api
{url=https://jsonplaceholder.typicode.com/users : The URL of the JSON API}
{--limit= : Limit the number of users to import}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Import users from a JSON API';

/**
* Execute the console command.
*/
public function handle()
{
$url = $this->argument('url');
$limit = $this->option('limit');

$this->info("Importing users from {$url}");

try {
// Fetch users from the API
$response = Http::get($url);

if ($response->failed()) {
$this->error("Failed to fetch users from the API: {$response->status()}");
return 1;
}

$users = $response->json();

if (empty($users)) {
$this->warn("No users found in the API response.");
return 0;
}

// Apply limit if specified
if ($limit && is_numeric($limit)) {
$users = array_slice($users, 0, (int) $limit);
$this->info("Limiting import to {$limit} users.");
}

$this->info("Found " . count($users) . " users to import.");

$importedCount = 0;
$skippedCount = 0;
$errorCount = 0;

$progressBar = $this->output->createProgressBar(count($users));
$progressBar->start();

foreach ($users as $userData) {
// Validate user data
$validator = Validator::make($userData, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
]);

if ($validator->fails()) {
$this->newLine();
$this->warn("Skipping invalid user data: " . json_encode($userData));
$errorCount++;
$progressBar->advance();
continue;
}

// Check if user already exists
$existingUser = User::where('email', $userData['email'])->first();

if ($existingUser) {
$skippedCount++;
$progressBar->advance();
continue;
}

// Create new user
try {
User::create([
'name' => $userData['name'],
'email' => $userData['email'],
'password' => Hash::make(Str::random(16)), // Generate a random password
'email_verified_at' => now(),
]);

$importedCount++;
} catch (\Exception $e) {
$this->newLine();
$this->error("Error creating user {$userData['email']}: " . $e->getMessage());
$errorCount++;
}

$progressBar->advance();
}

$progressBar->finish();
$this->newLine(2);

$this->info("Import completed:");
$this->info("- Imported: {$importedCount} users");
$this->info("- Skipped (already exist): {$skippedCount} users");
$this->info("- Errors: {$errorCount} users");

return 0;
} catch (\Exception $e) {
$this->error("An error occurred: " . $e->getMessage());
return 1;
}
}
}
50 changes: 44 additions & 6 deletions app/Http/Controllers/FavoriteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace App\Http\Controllers;

use App\Models\Post;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Requests\CreateFavoriteRequest;
use Illuminate\Http\Response;
use App\Http\Resources\FavoriteResource;
use App\Http\Resources\FavoritesCollection;

/**
* @group Favorites
Expand All @@ -16,20 +19,55 @@ class FavoriteController extends Controller
{
public function index(Request $request)
{
$favorites = $request->user()->favorites;
return FavoriteResource::collection($favorites);
$favorites = $request->user()->favorites()->with('favoritable')->get();
return new FavoritesCollection($favorites);
}

public function store(CreateFavoriteRequest $request, Post $post)
public function storePost(CreateFavoriteRequest $request, Post $post)
{
$request->user()->favorites()->create(['post_id' => $post->id]);
$request->user()->favorites()->create([
'favoritable_type' => Post::class,
'favoritable_id' => $post->id
]);

return response()->noContent(Response::HTTP_CREATED);
}

public function destroy(Request $request, Post $post)
public function destroyPost(Request $request, Post $post)
{
$favorite = $request->user()->favorites()->where('post_id', $post->id)->firstOrFail();
$favorite = $request->user()->favorites()
->where('favoritable_type', Post::class)
->where('favoritable_id', $post->id)
->firstOrFail();

$favorite->delete();

return response()->noContent();
}

public function storeUser(Request $request, User $user)
{
// Prevent users from favoriting themselves
if ($request->user()->id === $user->id) {
return response()->json([
'message' => 'You cannot favorite yourself'
], Response::HTTP_BAD_REQUEST);
}

$request->user()->favorites()->create([
'favoritable_type' => User::class,
'favoritable_id' => $user->id
]);

return response()->noContent(Response::HTTP_CREATED);
}

public function destroyUser(Request $request, User $user)
{
$favorite = $request->user()->favorites()
->where('favoritable_type', User::class)
->where('favoritable_id', $user->id)
->firstOrFail();

$favorite->delete();

Expand Down
63 changes: 57 additions & 6 deletions app/Http/Controllers/PostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

use App\Http\Resources\PostResource;
use App\Models\Post;
use App\Models\User;
use App\Http\Requests\CreatePostRequest;
use App\Http\Requests\UpdatePostRequest;
use App\Http\Requests\DestroyPostRequest;
use App\Notifications\NewPostByFavoritedUser;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;

/**
* @group Posts
Expand All @@ -24,13 +28,24 @@ public function index()
public function store(CreatePostRequest $request)
{
$user = $request->user();

// Create a new post
$post = Post::create([
$postData = [
'title' => $request->input('title'),
'body' => $request->input('body'),
'user_id' => $user->id,
]);
];

// Handle image upload if present
if ($request->hasFile('image')) {
$image = $request->file('image');
$imagePath = $image->store('posts', 'public');
$postData['image_path'] = $imagePath;
}

// Create a new post
$post = Post::create($postData);

// Find all users who have favorited this user
$this->notifyFollowers($user, $post);

return new PostResource($post);
}
Expand All @@ -42,18 +57,54 @@ public function show(Post $post)

public function update(UpdatePostRequest $request, Post $post)
{
$post->update([
$postData = [
'title' => $request->input('title'),
'body' => $request->input('body'),
]);
];

// Handle image upload if present
if ($request->hasFile('image')) {
// Delete old image if exists
if ($post->image_path) {
Storage::disk('public')->delete($post->image_path);
}

$image = $request->file('image');
$imagePath = $image->store('posts', 'public');
$postData['image_path'] = $imagePath;
}

$post->update($postData);

return new PostResource($post);
}

public function destroy(DestroyPostRequest $request, Post $post)
{
// Delete associated image if exists
if ($post->image_path) {
Storage::disk('public')->delete($post->image_path);
}

$post->delete();

return response()->noContent();
}

/**
* Notify followers about a new post
*/
protected function notifyFollowers(User $author, Post $post)
{
// Find all users who have favorited this author
$followers = User::whereHas('favorites', function ($query) use ($author) {
$query->where('favoritable_type', User::class)
->where('favoritable_id', $author->id);
})->get();

// Send notifications asynchronously
if ($followers->count() > 0) {
Notification::send($followers, new NewPostByFavoritedUser($post, $author));
}
}
}
1 change: 1 addition & 0 deletions app/Http/Requests/CreatePostRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function rules()
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'image' => 'nullable|image|mimes:jpg,jpeg,png,gif,webp|max:5120', // 5MB max
];
}
}
Loading