feat: v1.0 i guess lol
This commit is contained in:
parent
0ad6f8d833
commit
aa3b22e473
|
|
@ -39,7 +39,8 @@ class QueueNewArticleAlertForSubscribers extends Command
|
||||||
|
|
||||||
logger(count($articlesForNotification) . ' article(s) ready for notifying.');
|
logger(count($articlesForNotification) . ' article(s) ready for notifying.');
|
||||||
|
|
||||||
$subscribersToNotify = Subscriber::all();
|
// HARD CODE LIMIT 100 subscribers. (Kinda scared of gettings spammed and want to monitor it slowly)
|
||||||
|
$subscribersToNotify = Subscriber::limit(100)->get();
|
||||||
|
|
||||||
logger(count($subscribersToNotify) . ' subscribers(s) to notify.');
|
logger(count($subscribersToNotify) . ' subscribers(s) to notify.');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class WriterController extends Controller
|
||||||
|
|
||||||
public function newArticle(Request $request): View
|
public function newArticle(Request $request): View
|
||||||
{
|
{
|
||||||
return view('writer.new-article');
|
return view('writer.new-article', ['article' => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function livePreview(Request $request): View
|
public function livePreview(Request $request): View
|
||||||
|
|
@ -83,7 +83,7 @@ class WriterController extends Controller
|
||||||
|
|
||||||
public function saveArticle(Request $request): RedirectResponse
|
public function saveArticle(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$header_image_path = $this->storeHeaderImage($request);
|
// $header_image_path = $this->storeHeaderImage($request);
|
||||||
$article_empty = new Article();
|
$article_empty = new Article();
|
||||||
$article_empty->markdown_body = $request->get('markdown-body');
|
$article_empty->markdown_body = $request->get('markdown-body');
|
||||||
$article_empty->convertMarkdownToMarkup();
|
$article_empty->convertMarkdownToMarkup();
|
||||||
|
|
@ -95,13 +95,62 @@ class WriterController extends Controller
|
||||||
'subtitle' => $request->subtitle ?? null,
|
'subtitle' => $request->subtitle ?? null,
|
||||||
'body' => $article_empty->body,
|
'body' => $article_empty->body,
|
||||||
'published_at' => $request->get('publication-date') ?? null,
|
'published_at' => $request->get('publication-date') ?? null,
|
||||||
'header_image_url' => $header_image_path ? asset("storage/$header_image_path") : null,
|
// 'header_image_url' => $header_image_path ? asset("storage/$header_image_path") : null,
|
||||||
'markdown_body' => $request->get('markdown-body'),
|
'markdown_body' => $request->get('markdown-body'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect(route('dashboard'));
|
return redirect(route('dashboard'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function unpublishArticle(Article $article): RedirectResponse
|
||||||
|
{
|
||||||
|
$article->update(['published_at' => null]);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteArticle(Request $request, Article $article): RedirectResponse
|
||||||
|
{
|
||||||
|
$article->delete();
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editArticle(Request $request, Article $article): View
|
||||||
|
{
|
||||||
|
return view('writer.new-article', ['article' => $article]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function persistArticleEdits(Request $request, Article $article): RedirectResponse
|
||||||
|
{
|
||||||
|
// $header_image_path = $this->storeHeaderImage($request);
|
||||||
|
$article_empty = new Article();
|
||||||
|
$article_empty->markdown_body = $request->get('markdown-body');
|
||||||
|
$article_empty->convertMarkdownToMarkup();
|
||||||
|
|
||||||
|
$article->update([
|
||||||
|
'title' => $request->title ?? null,
|
||||||
|
'slug' => $request->slug ?? null,
|
||||||
|
'subtitle' => $request->subtitle ?? null,
|
||||||
|
'body' => $article_empty->body,
|
||||||
|
'published_at' => $request->get('publication-date') ?? null,
|
||||||
|
'markdown_body' => $request->get('markdown-body'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Article::create([
|
||||||
|
// 'title' => $request->title ?? null,
|
||||||
|
// 'slug' => $request->slug ?? null,
|
||||||
|
// 'author_id' => auth()->user()->id,
|
||||||
|
// 'subtitle' => $request->subtitle ?? null,
|
||||||
|
// 'body' => $article_empty->body,
|
||||||
|
// 'published_at' => $request->get('publication-date') ?? null,
|
||||||
|
// // 'header_image_url' => $header_image_path ? asset("storage/$header_image_path") : null,
|
||||||
|
// 'markdown_body' => $request->get('markdown-body'),
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
return redirect(route('edit-article', ['article' => $article->id]));
|
||||||
|
}
|
||||||
|
|
||||||
private function storeHeaderImage(Request $request): ?string
|
private function storeHeaderImage(Request $request): ?string
|
||||||
{
|
{
|
||||||
if(! $request->hasFile('header-image')) return null;
|
if(! $request->hasFile('header-image')) return null;
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@ use Illuminate\Support\Str;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class Article extends Model
|
class Article extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
/** The name of the database table storing this Model's records. */
|
/** The name of the database table storing this Model's records. */
|
||||||
protected $table = 'articles';
|
protected $table = 'articles';
|
||||||
|
|
@ -38,7 +40,14 @@ class Article extends Model
|
||||||
protected function publishedAt(): Attribute
|
protected function publishedAt(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
get: fn (string $value) => Carbon::parse($value)->format('D, M jS o, g:i A'),
|
get: fn (?string $value) => $value ? Carbon::parse($value)->format('D, M jS o, g:i A') : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function publishedAtRaw(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn (?string $value, array $attributes) => $attributes['published_at'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ return new class extends Migration
|
||||||
$table->dateTime('subscribers_notified_at')->nullable();
|
$table->dateTime('subscribers_notified_at')->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->softDeletes();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<th>Subhead</th>
|
<th>Subhead</th>
|
||||||
<th>Author</th>
|
<th>Author</th>
|
||||||
<th>Published At</th>
|
<th>Published At</th>
|
||||||
|
<th>Notifs Sent At</th>
|
||||||
<th>Last Updated</th>
|
<th>Last Updated</th>
|
||||||
<th>First Created</th>
|
<th>First Created</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
|
|
@ -29,23 +30,38 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $article->id }}</td>
|
<td>{{ $article->id }}</td>
|
||||||
<td>{{ $article?->slug ?? '-' }}</td>
|
<td>{{ $article?->slug ?? '-' }}</td>
|
||||||
<td>{{ $article?->title ?? '-' }}</td>
|
<td data-article-title-id="{{ $article->id }}">{{ $article?->title ?? '-' }}</td>
|
||||||
<td>{{ $article?->subtitle ?? '-' }}</td>
|
<td>{{ $article?->subtitle ?? '-' }}</td>
|
||||||
<td>{{ $article?->author?->display_name ?? '-' }}</td>
|
<td>{{ $article?->author?->display_name ?? '-' }}</td>
|
||||||
<td>{{ $article?->published_at ?? '-' }}</td>
|
<td>{{ $article?->published_at ?? '-' }}</td>
|
||||||
|
<td>{{ $article?->subscribers_notified_at ?? '-' }}</td>
|
||||||
<td>{{ $article?->updated_at ?? '-' }}</td>
|
<td>{{ $article?->updated_at ?? '-' }}</td>
|
||||||
<td>{{ $article?->created_at ?? '-' }}</td>
|
<td>{{ $article?->created_at ?? '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{--
|
{{--
|
||||||
<a href="{{ route() }}">
|
<a href="{{ }}">
|
||||||
<button type="button">Edit</button>
|
<form action="" method="POST">
|
||||||
|
<button type="submit">Edit</button>
|
||||||
|
</form>
|
||||||
</a>
|
</a>
|
||||||
--}}
|
--}}
|
||||||
<a href="{{}}">
|
|
||||||
<button type="button">
|
@if($article['published_at'])
|
||||||
Unpublish
|
<form action="{{ route('unpublish-article', ['article' => $article->id]) }}" method="POST">
|
||||||
</button>
|
@csrf
|
||||||
</a>
|
<input type="hidden" value="{{ $article->id }}">
|
||||||
|
<button type="submit">Unpublish</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('delete-article', ['article' => $article->id]) }}" method="POST" id="delete-article-form-{{ $article->id }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="delete-article-button" data-delete-article-id="{{ $article->id }}">Delete</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ route('edit-article', ['article' => $article->id]) }}" method="GET">
|
||||||
|
<button type="submit">Edit</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
|
|
@ -54,5 +70,23 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const buttons = document.getElementsByClassName('delete-article-button');
|
||||||
|
|
||||||
|
for(let i = 0; i < buttons.length; i++) {
|
||||||
|
const buttonElement = buttons[i];
|
||||||
|
|
||||||
|
buttonElement.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const articleId = event.target.dataset.deleteArticleId;
|
||||||
|
const articleTitle = document.querySelector(`td[data-article-title-id="${articleId}"]`)?.innerText;
|
||||||
|
const proceed = confirm(`You are about to delete the article titled: ${articleTitle}. Proceed? (It can technically be restored later if you ask the database admin nicely.)`);
|
||||||
|
if (proceed) {
|
||||||
|
document.getElementById(`delete-article-form-${articleId}`)?.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,22 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
gap: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#new-article-form {
|
#new-article-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#new-article-preview-container {
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 2em;
|
||||||
|
background-color: #000;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#markdown-body {
|
#markdown-body {
|
||||||
width: 40vw;
|
width: 40vw;
|
||||||
}
|
}
|
||||||
|
|
@ -74,20 +84,22 @@ document.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
|
||||||
<div id="page-body-container">
|
<div id="page-body-container">
|
||||||
<section id="new-article-form-container">
|
<section id="new-article-form-container">
|
||||||
<form id="new-article-form" method="POST" action="{{route('save_article')}}" enctype="multipart/form-data" hx-encoding="multipart/form-data">
|
<form id="new-article-form" method="POST" action="{{ $article ? route('save-article-edits', ['article' => $article->id]) : route('save-article') }}" enctype="multipart/form-data" hx-encoding="multipart/form-data">
|
||||||
@csrf
|
@csrf
|
||||||
<label for="publication-date">Publication Date</label>
|
<label for="publication-date">Publication Date</label>
|
||||||
<input id="publication-date" name="publication-date" type="datetime-local" class="affects-preview" />
|
<input id="publication-date" name="publication-date" type="datetime-local" class="affects-preview" value="{{ $article?->publishedAtRaw }}" />
|
||||||
<label for="title">Title</label>
|
<label for="title">Title</label>
|
||||||
<input id="title" name="title" type="text" class="affects-preview" />
|
<input id="title" name="title" type="text" class="affects-preview" value="{{ $article?->title }}" />
|
||||||
<label for="subtitle">Subtitle</label>
|
<label for="subtitle">Subtitle</label>
|
||||||
<input id="subtitle" name="subtitle" type="text" class="affects-preview" />
|
<input id="subtitle" name="subtitle" type="text" class="affects-preview" value="{{ $article?->subtitle }}" />
|
||||||
<label for="slug">Slug</label>
|
<label for="slug">Slug</label>
|
||||||
<input id="slug" name="slug" type="text" class="affects-preview" />
|
<input id="slug" name="slug" type="text" class="affects-preview" value="{{ $article?->slug }}" />
|
||||||
|
<!-- Header images disabled for now
|
||||||
<label for="header-image">Header Image</label>
|
<label for="header-image">Header Image</label>
|
||||||
<input id="header-image" name="header-image" type="file" accept="image/png, image/jpeg, image/jpg, image/webp" class="affects-preview" />
|
<input id="header-image" name="header-image" type="file" accept="image/png, image/jpeg, image/jpg, image/webp" class="affects-preview" />
|
||||||
|
-->
|
||||||
<label for="markdown-body">Markdown Body</label>
|
<label for="markdown-body">Markdown Body</label>
|
||||||
<textarea id="markdown-body" rows="12" name="markdown-body" placeholder="Github-flavored markdown is supported!" class="affects-preview"></textarea>
|
<textarea id="markdown-body" rows="25" name="markdown-body" placeholder="Github-flavored markdown is supported!" class="affects-preview">{{ $article?->markdown_body }}</textarea>
|
||||||
<button
|
<button
|
||||||
id="preview-article"
|
id="preview-article"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,9 @@ Route::get('/post/{article_slug}', function ($article_slug) {
|
||||||
})->name('article');
|
})->name('article');
|
||||||
|
|
||||||
Route::get('/writer/login', function () {
|
Route::get('/writer/login', function () {
|
||||||
|
if (auth()->check()) {
|
||||||
|
return redirect(route('dashboard'));
|
||||||
|
}
|
||||||
return view('writer.login');
|
return view('writer.login');
|
||||||
})->name('login');
|
})->name('login');
|
||||||
|
|
||||||
|
|
@ -85,5 +88,9 @@ Route::middleware('auth')->group(function () {
|
||||||
Route::post('/logout', [WriterController::class, 'logout'])->name('logout');
|
Route::post('/logout', [WriterController::class, 'logout'])->name('logout');
|
||||||
Route::get('writer/new-article', [WriterController::class, 'newArticle'])->name('new_article');
|
Route::get('writer/new-article', [WriterController::class, 'newArticle'])->name('new_article');
|
||||||
Route::post('writer/live-preview', [WriterController::class, 'livePreview'])->name('live_preview');
|
Route::post('writer/live-preview', [WriterController::class, 'livePreview'])->name('live_preview');
|
||||||
Route::post('writer/save-article', [WriterController::class, 'saveArticle'])->name('save_article');
|
Route::post('writer/save-article', [WriterController::class, 'saveArticle'])->name('save-article');
|
||||||
|
Route::post('writer/unpublish-article/{article:id}', [WriterController::class, 'unpublishArticle'])->name('unpublish-article');
|
||||||
|
Route::post('writer/articles/{article:id}', [WriterController::class, 'deleteArticle'])->name('delete-article');
|
||||||
|
Route::get('writer/edit-article/{article:id}', [WriterController::class, 'editArticle'])->name('edit-article');
|
||||||
|
Route::post('writer/edit-article/{article:id}', [WriterController::class, 'persistArticleEdits'])->name('save-article-edits');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue