Skip to content
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"neuron-php/mvc": "0.8.*",
"neuron-php/cli": "0.8.*",
"neuron-php/jobs": "0.2.*",
"neuron-php/orm": "0.1.*",
"phpmailer/phpmailer": "^6.9"
},
"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion resources/views/admin/dashboard/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<p><strong>Role:</strong> <?= htmlspecialchars($User->getRole()) ?></p>
<p><strong>Account Status:</strong> <?= htmlspecialchars($User->getStatus()) ?></p>
<?php if($User->getLastLoginAt()): ?>
<p class="mb-0"><strong>Last Login:</strong> <?= $User->getLastLoginAt()->format('F j, Y g:i A') ?></p>
<p class="mb-0"><strong>Last Login:</strong> <?= format_user_datetime($User->getLastLoginAt(), 'F j, Y g:i A') ?></p>
<?php endif; ?>
</div>
</div>
2 changes: 1 addition & 1 deletion resources/views/admin/posts/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<td><?= $post->getAuthor() ? htmlspecialchars( $post->getAuthor()->getUsername() ) : 'Unknown' ?></td>
<td><span class="badge bg-<?= $post->getStatus() === 'published' ? 'success' : 'secondary' ?>"><?= htmlspecialchars( $post->getStatus() ) ?></span></td>
<td><?= $post->getViewCount() ?></td>
<td><?= $post->getCreatedAt() ? $post->getCreatedAt()->format( 'Y-m-d H:i' ) : 'N/A' ?></td>
<td><?= format_user_datetime( $post->getCreatedAt() ) ?></td>
<td>
<a href="<?= route_path('blog_post', ['slug' => $post->getSlug()]) ?>" class="btn btn-sm btn-outline-secondary" target="_blank">View</a>
<a href="<?= route_path('admin_posts_edit', ['id' => $post->getId()]) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
Expand Down
43 changes: 43 additions & 0 deletions resources/views/admin/profile/edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,49 @@
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars( $User->getEmail() ) ?>" required>
</div>

<div class="mb-3">
<label for="timezone" class="form-label">Timezone</label>
<select class="form-select" id="timezone" name="timezone">
<?php
$currentTimezone = $User->getTimezone();
$grouped = [];

// Group timezones by region
$grouped['Other'] = [];
foreach( $timezones as $timezone )
{
$parts = explode( '/', $timezone, 2 );
if( count( $parts ) === 2 )
{
$region = $parts[0];
if( !isset( $grouped[$region] ) )
{
$grouped[$region] = [];
}
$grouped[$region][] = $timezone;
}
else
{
$grouped['Other'][] = $timezone;
}
}

// Display grouped timezones
foreach( $grouped as $region => $tzList )
{
echo '<optgroup label="' . htmlspecialchars( $region ) . '">';
foreach( $tzList as $timezone )
{
$selected = $timezone === $currentTimezone ? ' selected' : '';
echo '<option value="' . htmlspecialchars( $timezone ) . '"' . $selected . '>' . htmlspecialchars( $timezone ) . '</option>';
}
echo '</optgroup>';
}
?>
</select>
<small class="form-text text-muted">All dates and times will be displayed in your selected timezone</small>
</div>

<div class="mb-3">
<label class="form-label">Role</label>
<input type="text" class="form-control" value="<?= htmlspecialchars( ucfirst( $User->getRole() ) ) ?>" disabled>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/admin/users/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<td><?= htmlspecialchars( $user->getEmail() ) ?></td>
<td><span class="badge bg-primary"><?= htmlspecialchars( $user->getRole() ) ?></span></td>
<td><span class="badge bg-<?= $user->getStatus() === 'active' ? 'success' : 'secondary' ?>"><?= htmlspecialchars( $user->getStatus() ) ?></span></td>
<td><?= $user->getCreatedAt() ? $user->getCreatedAt()->format( 'Y-m-d H:i' ) : 'N/A' ?></td>
<td><?= format_user_datetime( $user->getCreatedAt() ) ?></td>
<td>
<a href="<?= route_path('admin_users_edit', ['id' => $user->getId()]) ?>" class="btn btn-sm btn-outline-primary">Edit</a>
<?php if( $User->getId() !== $user->getId() ): ?>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/blog/category.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<p class="text-muted">
<small>
By <?= $post->getAuthor() ? htmlspecialchars( $post->getAuthor()->getUsername() ) : 'Unknown' ?>
on <?= $post->getCreatedAt() ? $post->getCreatedAt()->format( 'F j, Y' ) : '' ?>
on <?= format_user_date( $post->getCreatedAt(), 'F j, Y' ) ?>
</small>
</p>
<?php if( $post->getExcerpt() ): ?>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/blog/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<small>
<?php $Author = $Post->getAuthor(); ?>
By <?= htmlspecialchars( $Author ? $Author->getUsername() : 'Unknown' ) ?>
on <?= $Post->getCreatedAt() ? $Post->getCreatedAt()->format( 'F j, Y' ) : '' ?>
on <?= format_user_date( $Post->getCreatedAt(), 'F j, Y' ) ?>
<?php if( $Post->getViewCount() > 0 ): ?>
· <?= $Post->getViewCount() ?> views
<?php endif; ?>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/blog/show.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<p class="text-muted">
<?php $Author = $Post->getAuthor(); ?>
By <?= htmlspecialchars( $Author ? $Author->getUsername() : 'Unknown' ) ?>
on <?= $Post->getCreatedAt() ? $Post->getCreatedAt()->format( 'F j, Y' ) : '' ?>
on <?= format_user_date( $Post->getCreatedAt(), 'F j, Y' ) ?>
<?php if( $Post->getViewCount() > 0 ): ?>
· <?= $Post->getViewCount() ?> views
<?php endif; ?>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/blog/tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<p class="text-muted">
<small>
By <?= $post->getAuthor() ? htmlspecialchars( $post->getAuthor()->getUsername() ) : 'Unknown' ?>
on <?= $post->getCreatedAt() ? $post->getCreatedAt()->format( 'F j, Y' ) : '' ?>
on <?= format_user_date( $post->getCreatedAt(), 'F j, Y' ) ?>
</small>
</p>
<?php if( $post->getExcerpt() ): ?>
Expand Down
25 changes: 22 additions & 3 deletions src/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
use Neuron\Data\Object\Version;
use Neuron\Data\Setting\Source\Yaml;
use Neuron\Mvc\Application;
use Neuron\Orm\Model;
use Neuron\Cms\Database\ConnectionFactory;

// Load authentication helper functions
require_once __DIR__ . '/Cms/Auth/helpers.php';

/**
* CMS Bootstrap Module for the Neuron Framework
*
*
* This module provides bootstrap functionality for initializing Neuron CMS
* applications. It serves as the entry point for CMS-specific configuration
* and setup, extending the base MVC application with content management
* capabilities.
*
*
* @package Neuron\Cms
*/

Expand All @@ -27,12 +29,29 @@
* and site configuration. It delegates to the MVC boot function but
* provides CMS-specific context and naming.
*
* Additionally initializes the ORM with the database connection from settings.
*
* @param string $configPath Path to the CMS configuration directory
* @return Application Fully configured CMS application instance
* @throws \Exception If configuration loading or application initialization fails
*/

function boot( string $configPath ) : Application
{
return \Neuron\Mvc\boot( $configPath );
$app = \Neuron\Mvc\boot( $configPath );

// Initialize ORM with PDO connection from settings
try
{
$pdo = ConnectionFactory::createFromSettings( $app->getSettingManager() );
Model::setPdo( $pdo );
}
catch( \Exception $e )
{
// If database configuration is missing or invalid, log but don't fail
// This allows the application to start even without database
error_log( 'CMS ORM initialization warning: ' . $e->getMessage() );
}

return $app;
}
1 change: 1 addition & 0 deletions src/Cms/Cli/Commands/Install/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ public function change()
->addColumn( 'failed_login_attempts', 'integer', [ 'default' => 0 ] )
->addColumn( 'locked_until', 'timestamp', [ 'null' => true ] )
->addColumn( 'last_login_at', 'timestamp', [ 'null' => true ] )
->addColumn( 'timezone', 'string', [ 'limit' => 50, 'default' => 'UTC' ] )
->addColumn( 'created_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP' ] )
->addColumn( 'updated_at', 'timestamp', [ 'default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP' ] )
->addIndex( [ 'username' ], [ 'unique' => true ] )
Expand Down
8 changes: 7 additions & 1 deletion src/Cms/Controllers/Admin/ProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,14 @@ public function edit( array $parameters ): string
$csrfManager = new CsrfTokenManager( $this->getSessionManager() );
Registry::getInstance()->set( 'Auth.CsrfToken', $csrfManager->getToken() );

// Get available timezones grouped by region
$timezones = \DateTimeZone::listIdentifiers();

$viewData = [
'Title' => 'Profile | ' . $this->getName(),
'Description' => 'Edit Your Profile',
'User' => $user,
'timezones' => $timezones,
'success' => $this->getSessionManager()->getFlash( 'success' ),
'error' => $this->getSessionManager()->getFlash( 'error' )
];
Expand Down Expand Up @@ -91,6 +95,7 @@ public function update( array $parameters ): never
}

$email = $_POST['email'] ?? '';
$timezone = $_POST['timezone'] ?? '';
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
Expand Down Expand Up @@ -118,7 +123,8 @@ public function update( array $parameters ): never
$user->getUsername(),
$email,
$user->getRole(),
!empty( $newPassword ) ? $newPassword : null
!empty( $newPassword ) ? $newPassword : null,
!empty( $timezone ) ? $timezone : null
);
$this->redirect( 'admin_profile', [], ['success', 'Profile updated successfully'] );
}
Expand Down
13 changes: 10 additions & 3 deletions src/Cms/Models/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

use DateTimeImmutable;
use Exception;
use Neuron\Orm\Model;
use Neuron\Orm\Attributes\{Table, BelongsToMany};

/**
* Category entity representing a blog post category.
*
* @package Neuron\Cms\Models
*/
class Category
#[Table('categories')]
class Category extends Model
{
private ?int $_id = null;
private string $_name;
Expand All @@ -19,6 +22,10 @@ class Category
private ?DateTimeImmutable $_createdAt = null;
private ?DateTimeImmutable $_updatedAt = null;

// Relationships
#[BelongsToMany(Post::class, pivotTable: 'post_categories')]
private array $_posts = [];

public function __construct()
{
$this->_createdAt = new DateTimeImmutable();
Expand Down Expand Up @@ -130,10 +137,10 @@ public function setUpdatedAt( ?DateTimeImmutable $updatedAt ): self
* Create Category from array data
*
* @param array $data Associative array of category data
* @return Category
* @return static
* @throws Exception
*/
public static function fromArray( array $data ): Category
public static function fromArray( array $data ): static
{
$category = new self();

Expand Down
16 changes: 12 additions & 4 deletions src/Cms/Models/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

use DateTimeImmutable;
use Exception;
use Neuron\Orm\Model;
use Neuron\Orm\Attributes\{Table, BelongsTo, BelongsToMany};

/**
* Post entity representing a blog post.
*
* @package Neuron\Cms\Models
*/
class Post
#[Table('posts')]
class Post extends Model
{
private ?int $_id = null;
private string $_title;
Expand All @@ -25,9 +28,14 @@ class Post
private ?DateTimeImmutable $_createdAt = null;
private ?DateTimeImmutable $_updatedAt = null;

// Relationships - these will be populated by the repository
// Relationships - now managed by ORM
#[BelongsTo(User::class, foreignKey: 'author_id')]
private ?User $_author = null;

#[BelongsToMany(Category::class, pivotTable: 'post_categories')]
private array $_categories = [];

#[BelongsToMany(Tag::class, pivotTable: 'post_tags')]
private array $_tags = [];

/**
Expand Down Expand Up @@ -424,10 +432,10 @@ public function hasTag( Tag $tag ): bool
* Create Post from array data
*
* @param array $data Associative array of post data
* @return Post
* @return static
* @throws Exception
*/
public static function fromArray( array $data ): Post
public static function fromArray( array $data ): static
{
$post = new self();

Expand Down
13 changes: 10 additions & 3 deletions src/Cms/Models/Tag.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@

use DateTimeImmutable;
use Exception;
use Neuron\Orm\Model;
use Neuron\Orm\Attributes\{Table, BelongsToMany};

/**
* Tag entity representing a blog post tag.
*
* @package Neuron\Cms\Models
*/
class Tag
#[Table('tags')]
class Tag extends Model
{
private ?int $_id = null;
private string $_name;
private string $_slug;
private ?DateTimeImmutable $_createdAt = null;
private ?DateTimeImmutable $_updatedAt = null;

// Relationships
#[BelongsToMany(Post::class, pivotTable: 'post_tags')]
private array $_posts = [];

public function __construct()
{
$this->_createdAt = new DateTimeImmutable();
Expand Down Expand Up @@ -112,10 +119,10 @@ public function setUpdatedAt( ?DateTimeImmutable $updatedAt ): self
* Create Tag from array data
*
* @param array $data Associative array of tag data
* @return Tag
* @return static
* @throws Exception
*/
public static function fromArray( array $data ): Tag
public static function fromArray( array $data ): static
{
$tag = new self();

Expand Down
Loading