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
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[docker-compose.yml]
indent_size = 4
75 changes: 75 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
APP_NAME="Article Crud"
APP_ENV=local
APP_KEY=base64:UfXqKchu5dj4hrHMQX7NA554kgxYYoZQ+mFbL2pKXtM=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=article_crud
DB_USERNAME=root
DB_PASSWORD=your_password_here

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

DEBUGBAR_ENABLED=true

DEFAULT_USER_NAME="Blogger"
DEFAULT_USER_EMAIL="blogger@blogger.website"
DEFAULT_USER_PASSWORD="123456789"
CATEGORIES_TO_SEED=12
TAGS_TO_SEED=18
ARTICLES_TO_SEED=40
MIN_TAGS_PER_ARTICLE_TO_SEED=0
MAX_TAGS_PER_ARTICLE_TO_SEED=7
PERCENTAGE_OF_ARTICLES_WITH_IMAGES=40
CATEGORIES_PER_PAGE=10
TAGS_PER_PAGE=7
ARTICLES_PER_PAGE=8
MAX_CHARS_ARTICLE_TEXT=300
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
* text=auto eol=lf

*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php

/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/.phpunit.cache
/node_modules
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/gitignore
app/Http/Requests/Admin/*.txt
/resources/views/admin/articles/*.txt
/routes/web_dev.php
/tests0
leiame.txt
93 changes: 19 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,32 @@
# Laravel Roadmap: Beginner Level Challenge
# Blogger's Home Page

This is a task for the [Beginner Level of the Laravel Roadmap](https://github.com/LaravelDaily/Laravel-Roadmap-Learning-Path#beginner-level), with the goal to implement as many of its topics as possible.
![Laravel](https://img.shields.io/badge/laravel-%23FF2D20.svg?style=for-the-badge&logo=laravel&logoColor=white)
![PHP](https://img.shields.io/badge/php-%23777BB4.svg?style=for-the-badge&logo=php&logoColor=white)
![MySQL](https://img.shields.io/badge/mysql-%2300f.svg?style=for-the-badge&logo=mysql&logoColor=white)
![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white)
![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white)
![DigitalOcean](https://img.shields.io/badge/DigitalOcean-%230167ff.svg?style=for-the-badge&logo=digitalOcean&logoColor=white)

This repository is intentionally empty, with only a Readme file. Your task if to submit a Pull Request with your version of implementing the task, and your PR may be reviewed by someone on our team, or other volunteers.
This is a blogger web app for only one registered user who is both the blogger and admin. Public users can read the articles posted, and select which article he/she wishes to read by navigating on the main page. The admin and blogger can manage categories, tags as well as the articles themselves.

## The Task: Simple Personal Blog
This project is an answer to the **[Laravel Roadmap: Beginner Level Challenge](https://github.com/LaravelDaily/Laravel-Roadmap-Beginner-Challenge)**.

You need to create a personal blog with just three pages:
This project has been taken offline on Oct. 17th, 2023. The server is minimal: 10GB SSD, 0.5GB RAM. Yet the queries are performant. Images below:

- Homepage: List of articles
- Article page
- Some static text page like "About me"
![main page](/gitimages/image01.png)

![article editing page](/gitimages/image02.png)

Also, there should be a Login mechanism (but no Register) for the author to write articles:
## Instalation

- Manage (meaning, create/update/delete) categories
- Manage tags
- Manage articles
- For Auth Starter Kit, use [Laravel Breeze](https://github.com/laravel/breeze) (Tailwind) or [Laravel UI](https://github.com/laravel/ui) (Bootstrap) - that starter kit will have some design, which is enough: the design is irrelevant for accomplishing the task
### Forge Install

Deploy the server and the code. Run the command `php artisan db:seed`. Schedule the unused image cleansing job with this command (or similar): `php /home/forge/default/artisan app:purge-garbage-images` to run every minute or whenever.

**DB Structure:**
### Local Install

- Article has title (required), full text (required) and image to upload (optional)
- Article may have only one category, but may have multiple tags
Create a new MySQL schema. Name it according to your .env file. Run the command `php artisan migrate --seed`. Optional: Schedule the unused image cleansing command with `php artisan app:purge-garbage-files` to run every minute or whenever. Run this app as usual.

*It is crucial to run at least the UserSeeder, to seed the database with the only user.* That user is described in the `.env` / `.env.example` file.

-----

## Features to implement

Here's the [list of Roadmap features](https://github.com/LaravelDaily/Laravel-Roadmap-Learning-Path#beginner-level) you need to try to implement in your code:

**Routing and Controllers: Basics**

- Callback Functions and Route::view()
- Routing to a Single Controller Method
- Route Parameters
- Route Naming
- Route Groups


**Blade Basics**

- Displaying Variables in Blade
- Blade If-Else and Loop Structures
- Blade Loops
- Layout: @include, @extends, @section, @yield
- Blade Components


**Auth Basics**

- Default Auth Model and Access its Fields from Anywhere
- Check Auth in Controller / Blade
- Auth Middleware


**Database Basics**

- Database Migrations
- Basic Eloquent Model and MVC: Controller -> Model -> View
- Eloquent Relationships: belongsTo / hasMany / belongsToMany
- Eager Loading and N+1 Query Problem


**Full Simple CRUD**

- Route Resource and Resourceful Controllers
- Forms, Validation and Form Requests
- File Uploads and Storage Folder Basics
- Table Pagination


-----

## Example Solutions

If you need help, or you want to compare your version with our simple version, here are two public repositories with the solution:

- [Laravel Roadmap Beginner: Breeze](https://github.com/LaravelDaily/Laravel-Roadmap-Beginner-Roadmap-Breeze)
- [Laravel Roadmap Beginner: UI](https://github.com/LaravelDaily/Laravel-Roadmap-Beginner-Blog-UI)


**Notice**: please look at those repositories only AFTER you've accomplished the task yourself, or if you're confident about your Laravel beginner skills and you think you don't need to practice this task.
*It is not necessary to run the command* `php artisan storage:link`*.*
34 changes: 34 additions & 0 deletions app/Console/Commands/GenerateGarbageFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use App\Helpers\FilesystemHelper;

class GenerateGarbageFiles extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:generate-garbage-files';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generates a bunch of garbage files. For testing. To be deleted afterwards with PurgeGarbageImages.';

/**
* Execute the console command.
*/
public function handle() {
$n = rand(3,9);
for ($i=0; $i<$n; $i++) {
Storage::disk(FilesystemHelper::DISK_FOR_UPLOADS)->put('garbage' . strval($i) . '.txt', fake()->text(80));
}
$this->info(strval($n) . ' garbage files successfully generated.');
}
}
55 changes: 55 additions & 0 deletions app/Console/Commands/PurgeGarbageImages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use App\Helpers\FilesystemHelper;

/* This command is meant to be run as a cron job. Run it every minute, or every other minute; or whenever. */

class PurgeGarbageImages extends Command {
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:purge-garbage-images';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Purges any unused and undesired uploaded images.';

/**
* Execute the console command.
*/
public function handle() {
$filesystemImages = collect(Storage::disk(FilesystemHelper::DISK_FOR_UPLOADS)->files());
$filesystemImages->transform(function (string $item, int $key) {
return FilesystemHelper::DISK_FOR_UPLOADS . '/' . $item;
});
//var_dump($filesystemImages);
$dbImages = DB::table('articles')
->select('image')
->whereNotNull('image')
->whereRaw("image like '" . FilesystemHelper::DISK_FOR_UPLOADS . "%'")
->distinct()
->get()
->pluck('image');
//var_dump($dbImages);
$diffImages = $filesystemImages->diff($dbImages);
//var_dump($diffImages);
$diffImages->transform(function (string $item, int $key) {
return substr($item, strlen(FilesystemHelper::DISK_FOR_UPLOADS) + 1);
});
//var_dump($diffImages);
$diffImages->each(function (string $item, int $key) {
Storage::disk(FilesystemHelper::DISK_FOR_UPLOADS)->delete($item);
});
$this->info(strval($diffImages->count()) . ' undesirable image(s) removed.');
}
}
27 changes: 27 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}

/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');

require base_path('routes/console.php');
}
}
30 changes: 30 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;

class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];

/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}
Loading