diff --git a/etc/db/migration-1.0.3.php b/etc/db/migration-1.0.3.php new file mode 100644 index 0000000..8679fa8 --- /dev/null +++ b/etc/db/migration-1.0.3.php @@ -0,0 +1,135 @@ +exec(" + CREATE TABLE IF NOT EXISTS `landing_page_content` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `section` varchar(50) NOT NULL, + `field_key` varchar(100) NOT NULL, + `field_value` text DEFAULT NULL, + `field_type` enum('string','rich','image') NOT NULL DEFAULT 'string', + `created_at` timestamp NULL DEFAULT current_timestamp(), + `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `section_field` (`section`, `field_key`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +"); + +// Insert default content using prepared statements (handles special characters properly) +$stmt = $pdo->prepare(" + INSERT INTO `landing_page_content` (`section`, `field_key`, `field_value`, `field_type`) + VALUES (:section, :field_key, :field_value, :field_type) +"); + +// Home Section +$stmt->execute([ + 'section' => 'home', + 'field_key' => 'hero_subtitle', + 'field_value' => 'A New Era of Chaos Begins', + 'field_type' => 'string' +]); + +$stmt->execute([ + 'section' => 'home', + 'field_key' => 'hero_description', + 'field_value' => 'Dive into an epic multiverse where heroes unite against the forces of chaos. From the depths of the Skibidi universe to the realms of Marvel, DC, Star Wars, and beyond.', + 'field_type' => 'rich' +]); + +// About Section +$stmt->execute([ + 'section' => 'about', + 'field_key' => 'title', + 'field_value' => 'The Story Unfolds', + 'field_type' => 'string' +]); + +$aboutText = '
+ Welcome to Skibidi Madness - an extraordinary animation series created by FireStormX Studios + that transcends the boundaries of the original Skibidi Toilet universe. This isn\'t just another story; + it\'s a revolutionary fusion of multiple dimensions, timelines, and realities. It\'s a celebration of creativity, imagination, and the limitless possibilities + of storytelling. +
++ In this new saga, witness the unprecedented chaos unleashed by the malevolent forces known as the + Asotra. Unlike previous battles against entire armies, our heroes now face their + most formidable adversary yet - the mysterious and powerful Supreme Leader, + whose ambitions threaten not just one universe, but the entire multiverse fabric. +
++ Skibidi Madness weaves together elements from beloved franchises including Marvel\'s cosmic battles, + the supernatural mysteries of Stranger Things, DC\'s legendary heroes, the epic space opera of Star Wars, + the blocky realms of Minecraft, and countless other dimensions. This is where everything you love + collides in spectacular fashion. +
'; + +$stmt->execute([ + 'section' => 'about', + 'field_key' => 'about_text', + 'field_value' => $aboutText, + 'field_type' => 'rich' +]); + +// About Section - Image (default to existing static image path) +$stmt->execute([ + 'section' => 'about', + 'field_key' => 'image', + 'field_value' => '/public/media/img/all-together.png', + 'field_type' => 'image' +]); + +// Episodes Section +$stmt->execute([ + 'section' => 'episodes', + 'field_key' => 'title', + 'field_value' => 'Featured Episodes', + 'field_type' => 'string' +]); + +$stmt->execute([ + 'section' => 'episodes', + 'field_key' => 'subtitle', + 'field_value' => 'Watch the epic battles and witness the chaos unfold', + 'field_type' => 'rich' +]); + +// Heroes Section +$stmt->execute([ + 'section' => 'heroes', + 'field_key' => 'title', + 'field_value' => 'The Legendary Heroes', + 'field_type' => 'string' +]); + +$stmt->execute([ + 'section' => 'heroes', + 'field_key' => 'subtitle', + 'field_value' => 'Meet the champions who stand between order and absolute chaos', + 'field_type' => 'rich' +]); + +// Channel Section +$stmt->execute([ + 'section' => 'channel', + 'field_key' => 'title', + 'field_value' => 'Join the FireStormX Community', + 'field_type' => 'string' +]); + +$stmt->execute([ + 'section' => 'channel', + 'field_key' => 'subtitle', + 'field_value' => 'Subscribe to FireStormX Studios on YouTube to never miss an episode of Skibidi Madness! Get exclusive behind-the-scenes content, character reveals, and be part of the growing community of fans exploring the multiverse.', + 'field_type' => 'rich' +]); + +return true; diff --git a/etc/db/schema.sql b/etc/db/schema.sql index a21439d..e845e78 100644 --- a/etc/db/schema.sql +++ b/etc/db/schema.sql @@ -52,6 +52,16 @@ CREATE TABLE `heroes` ( `enabled` tinyint(1) DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE `landing_page_content` ( + `id` int(11) NOT NULL, + `section` varchar(50) NOT NULL, + `field_key` varchar(100) NOT NULL, + `field_value` text DEFAULT NULL, + `field_type` enum('string','rich','image') NOT NULL DEFAULT 'string', + `created_at` timestamp NULL DEFAULT current_timestamp(), + `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE `migrations` ( `id` int(11) NOT NULL, `version` int(11) NOT NULL, @@ -128,6 +138,10 @@ ALTER TABLE `heroes` ADD KEY `idx_slug` (`slug`), ADD KEY `idx_order` (`display_order`); +ALTER TABLE `landing_page_content` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `section_field` (`section`, `field_key`); + ALTER TABLE `migrations` ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `version` (`version`); @@ -164,6 +178,9 @@ ALTER TABLE `episodes` ALTER TABLE `heroes` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +ALTER TABLE `landing_page_content` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; + ALTER TABLE `migrations` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; diff --git a/src/Controller/Admin/Landing.php b/src/Controller/Admin/Landing.php new file mode 100644 index 0000000..eab21e8 --- /dev/null +++ b/src/Controller/Admin/Landing.php @@ -0,0 +1,23 @@ +render( + 'admin/landing', + [ + 'sections' => $sections, + 'sectionsConfig' => $sectionsConfig, + ] + ); + } +} diff --git a/src/Controller/Admin/Landing/Update.php b/src/Controller/Admin/Landing/Update.php new file mode 100644 index 0000000..5f144d4 --- /dev/null +++ b/src/Controller/Admin/Landing/Update.php @@ -0,0 +1,93 @@ +getRequest()->isPost()) { + $this->updateContent(); + $this->redirect('/admin/landing'); + } + + // Redirect to landing page if accessed via GET + $this->redirect('/admin/landing'); + } + + protected function updateContent(): void + { + $landingData = $this->getRequest()->post('landing'); + + if (!is_array($landingData)) { + return; + } + + $sectionsConfig = LandingPageContent::getSectionsConfig(); + + foreach ($landingData as $section => $fields) { + if (!isset($sectionsConfig[$section])) { + continue; + } + + foreach ($fields as $fieldKey => $value) { + if (!isset($sectionsConfig[$section]['fields'][$fieldKey])) { + continue; + } + + $fieldType = $sectionsConfig[$section]['fields'][$fieldKey]['type']; + + // Handle image uploads + if ($fieldType === LandingPageContent::FIELD_TYPE_IMAGE) { + $uploadedImage = $this->handleImageUpload($section, $fieldKey); + if ($uploadedImage) { + $value = $uploadedImage; + } + } + + LandingPageContent::setValue($section, $fieldKey, $value, $fieldType); + } + } + } + + protected function handleImageUpload(string $section, string $fieldKey): ?string + { + $fileKey = "landing_image_{$section}_{$fieldKey}"; + $files = $this->getRequest()->files($fileKey); + + if (empty($files) || empty($files[0]['tmp_name'])) { + return null; + } + + $file = $files[0]; + + // Validate file + $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + if (!in_array($file['type'], $allowedTypes)) { + return null; + } + + // Generate upload path + $uploadDir = Config::get('upload_dir') . 'landing/'; + $fullPath = Config::get('root') . $uploadDir; + + if (!is_dir($fullPath)) { + mkdir($fullPath, 0755, true); + } + + // Generate unique filename + $extension = pathinfo($file['name'], PATHINFO_EXTENSION); + $filename = uniqid("{$section}_{$fieldKey}_", true) . '.' . $extension; + $filePath = $fullPath . $filename; + + if (move_uploaded_file($file['tmp_name'], $filePath)) { + return $uploadDir . $filename; + } + + return null; + } +} diff --git a/src/Controller/Admin/Social.php b/src/Controller/Admin/Social.php index 89f535f..69e7554 100644 --- a/src/Controller/Admin/Social.php +++ b/src/Controller/Admin/Social.php @@ -4,28 +4,44 @@ use App\Controller\AdminController; use App\Core\Model\CollectionInterface; -use App\Model\Episode; +use App\Model\SocialLink; class Social extends AdminController { public function handle(): void { + $collection = $this->getSocialLinks(); + + // Handle pagination + $page = max(1, (int)$this->getRequest('page', 1)); + $pageSize = (int)$this->getRequest('pageSize', 10); + + if ($pageSize < 1) { + $pageSize = 10; + } + + $collection->setPageSize($pageSize); + $collection->setPage($page); $this->render( - 'admin/episodes', + 'admin/social', [ - 'episodes' => $this->getPEpisodes() + 'socialLinks' => $collection, + 'currentPage' => $collection->getPage(), + 'totalPages' => $collection->getPages(), + 'pageSize' => $pageSize, + 'totalCount' => $collection->count() ] ); } - protected function getPEpisodes() + protected function getSocialLinks() { - $episodeCollection = (new Episode()) + $socialLinksCollection = (new SocialLink()) ->getCollection() ->setItemMode(CollectionInterface::ITEM_MODE_OBJECT) - ->sort('created_at', 'DESC'); + ->sort('display_order', 'ASC'); - return $episodeCollection; + return $socialLinksCollection; } } diff --git a/src/Controller/Admin/Social/Add.php b/src/Controller/Admin/Social/Add.php new file mode 100644 index 0000000..0a4761b --- /dev/null +++ b/src/Controller/Admin/Social/Add.php @@ -0,0 +1,19 @@ +render( + 'admin/social/form', + [ + 'socialLink' => new SocialLink() + ] + ); + } +} diff --git a/src/Controller/Admin/Social/Delete.php b/src/Controller/Admin/Social/Delete.php new file mode 100644 index 0000000..436205f --- /dev/null +++ b/src/Controller/Admin/Social/Delete.php @@ -0,0 +1,21 @@ +getRequest('id'); + + if ($id) { + $socialLink = new SocialLink(); + $socialLink->delete((int)$id); + } + + $this->redirect('/admin/social'); + } +} diff --git a/src/Controller/Admin/Social/Put.php b/src/Controller/Admin/Social/Put.php new file mode 100644 index 0000000..d102cdf --- /dev/null +++ b/src/Controller/Admin/Social/Put.php @@ -0,0 +1,37 @@ +getRequest()->isPost()) { + $this->createSocialLink(); + $this->redirect('/admin/social'); + } + + // Redirect if accessed via GET + $this->redirect('/admin/social/add'); + } + + protected function createSocialLink(): void + { + $data = $this->getRequest()->post('social'); + + if (!is_array($data)) { + return; + } + + unset($data['id']); + + // Handle enabled checkbox + $data['enabled'] = isset($data['enabled']) ? 1 : 0; + + $socialLink = new SocialLink(); + $socialLink->setData($data)->save(); + } +} diff --git a/src/Controller/Admin/Social/Update.php b/src/Controller/Admin/Social/Update.php new file mode 100644 index 0000000..52ffab0 --- /dev/null +++ b/src/Controller/Admin/Social/Update.php @@ -0,0 +1,50 @@ +getRequest()->isPost()) { + $this->updateSocialLink(); + $this->redirect('/admin/social'); + } + + $this->render( + 'admin/social/form', + [ + 'socialLink' => $this->findSocialLink() + ] + ); + } + + protected function findSocialLink(): SocialLink + { + $socialLink = new SocialLink(); + $socialLink->load($this->getRequest('id')); + + return $socialLink; + } + + protected function updateSocialLink(): void + { + $data = $this->getRequest()->post('social'); + + if (!is_array($data)) { + return; + } + + $socialLink = new SocialLink(); + $socialLink->load($data['id']); + unset($data['id']); + + // Handle enabled checkbox + $data['enabled'] = isset($data['enabled']) ? 1 : 0; + + $socialLink->setData($data)->save(); + } +} diff --git a/src/Controller/Index.php b/src/Controller/Index.php index 2108bf0..0f16a53 100644 --- a/src/Controller/Index.php +++ b/src/Controller/Index.php @@ -4,6 +4,8 @@ use App\Core\Controller; use App\Core\Model\Collection; use App\Model\BlogPost; +use App\Model\LandingPageContent; +use App\Model\SocialLink; class Index extends Controller { @@ -13,9 +15,37 @@ public function handle(): void 'episodes' => $this->getEpisodes(), 'blogPosts' => $this->getBlogPosts(), 'heroes' => $this->getHeroes(), + 'landingContent' => $this->getLandingContent(), + 'socialLinks' => $this->getSocialLinks(), ]); } + /** + * Retrieve landing page content from database + * + * @return array + */ + protected function getLandingContent(): array + { + return LandingPageContent::getAllSections(); + } + + /** + * Retrieve social links collection + * + * @return Collection + */ + protected function getSocialLinks(): Collection + { + $socialLinksCollection = (new SocialLink()) + ->getCollection() + ->setItemMode(Collection::ITEM_MODE_OBJECT) + ->addFilter(['enabled' => 1]) + ->sort('display_order', 'ASC'); + + return $socialLinksCollection; + } + /** * Retrieve heroes collection * diff --git a/src/Model/LandingPageContent.php b/src/Model/LandingPageContent.php new file mode 100644 index 0000000..74339c4 --- /dev/null +++ b/src/Model/LandingPageContent.php @@ -0,0 +1,213 @@ +query("SELECT * FROM " . self::TABLE . " ORDER BY section, field_key"); + $rows = $stmt->fetchAll(); + + $sections = []; + foreach ($rows as $row) { + if (!isset($sections[$row['section']])) { + $sections[$row['section']] = []; + } + $sections[$row['section']][$row['field_key']] = [ + 'id' => $row['id'], + 'value' => $row['field_value'], + 'type' => $row['field_type'], + ]; + } + + return $sections; + } + + /** + * Get content for a specific section. + * + * @param string $section Section name + * @return array Associative array with field_key as key + */ + public static function getSection(string $section): array + { + $pdo = Database::connect(); + $stmt = $pdo->prepare("SELECT * FROM " . self::TABLE . " WHERE section = :section"); + $stmt->execute(['section' => $section]); + $rows = $stmt->fetchAll(); + + $fields = []; + foreach ($rows as $row) { + $fields[$row['field_key']] = [ + 'id' => $row['id'], + 'value' => $row['field_value'], + 'type' => $row['field_type'], + ]; + } + + return $fields; + } + + /** + * Get a specific field value. + * + * @param string $section Section name + * @param string $fieldKey Field key + * @return string|null Field value or null if not found + */ + public static function getValue(string $section, string $fieldKey): ?string + { + $pdo = Database::connect(); + $stmt = $pdo->prepare( + "SELECT field_value FROM " . self::TABLE . " WHERE section = :section AND field_key = :field_key" + ); + $stmt->execute(['section' => $section, 'field_key' => $fieldKey]); + $result = $stmt->fetch(); + + return $result ? $result['field_value'] : null; + } + + /** + * Update or create a field value. + * + * @param string $section Section name + * @param string $fieldKey Field key + * @param string $value Field value + * @param string $type Field type (string or rich) + * @return bool Success status + */ + public static function setValue(string $section, string $fieldKey, string $value, string $type = self::FIELD_TYPE_STRING): bool + { + $pdo = Database::connect(); + + // Check if record exists + $stmt = $pdo->prepare( + "SELECT id FROM " . self::TABLE . " WHERE section = :section AND field_key = :field_key" + ); + $stmt->execute(['section' => $section, 'field_key' => $fieldKey]); + $existing = $stmt->fetch(); + + if ($existing) { + // Update + $stmt = $pdo->prepare( + "UPDATE " . self::TABLE . " SET field_value = :value, field_type = :type WHERE id = :id" + ); + return $stmt->execute(['value' => $value, 'type' => $type, 'id' => $existing['id']]); + } else { + // Insert + $stmt = $pdo->prepare( + "INSERT INTO " . self::TABLE . " (section, field_key, field_value, field_type) VALUES (:section, :field_key, :value, :type)" + ); + return $stmt->execute(['section' => $section, 'field_key' => $fieldKey, 'value' => $value, 'type' => $type]); + } + } + + /** + * Get available sections configuration. + * + * @return array Array of section configurations + */ + public static function getSectionsConfig(): array + { + return [ + self::SECTION_HOME => [ + 'title' => 'Home Section', + 'fields' => [ + 'hero_subtitle' => [ + 'label' => 'Hero Subtitle', + 'type' => self::FIELD_TYPE_STRING, + ], + 'hero_description' => [ + 'label' => 'Hero Description', + 'type' => self::FIELD_TYPE_RICH, + ], + ], + ], + self::SECTION_ABOUT => [ + 'title' => 'About Section', + 'fields' => [ + 'title' => [ + 'label' => 'Title', + 'type' => self::FIELD_TYPE_STRING, + ], + 'about_text' => [ + 'label' => 'About Text', + 'type' => self::FIELD_TYPE_RICH, + ], + 'image' => [ + 'label' => 'About Image', + 'type' => self::FIELD_TYPE_IMAGE, + ], + ], + ], + self::SECTION_EPISODES => [ + 'title' => 'Episodes Section', + 'fields' => [ + 'title' => [ + 'label' => 'Title', + 'type' => self::FIELD_TYPE_STRING, + ], + 'subtitle' => [ + 'label' => 'Subtitle', + 'type' => self::FIELD_TYPE_RICH, + ], + ], + ], + self::SECTION_HEROES => [ + 'title' => 'Heroes Section', + 'fields' => [ + 'title' => [ + 'label' => 'Title', + 'type' => self::FIELD_TYPE_STRING, + ], + 'subtitle' => [ + 'label' => 'Subtitle', + 'type' => self::FIELD_TYPE_RICH, + ], + ], + ], + self::SECTION_CHANNEL => [ + 'title' => 'Channel Section', + 'fields' => [ + 'title' => [ + 'label' => 'Title', + 'type' => self::FIELD_TYPE_STRING, + ], + 'subtitle' => [ + 'label' => 'Subtitle', + 'type' => self::FIELD_TYPE_RICH, + ], + ], + ], + ]; + } +} diff --git a/src/Model/SocialLink.php b/src/Model/SocialLink.php new file mode 100644 index 0000000..60afaef --- /dev/null +++ b/src/Model/SocialLink.php @@ -0,0 +1,38 @@ +get(self::COLUMN_ENABLED); + } + + public function getPlatform(): string + { + return $this->get(self::COLUMN_PLATFORM) ?? ''; + } + + public function getUrl(): string + { + return $this->get(self::COLUMN_URL) ?? ''; + } + + public function getIconClass(): string + { + return $this->get(self::COLUMN_ICON_CLASS) ?? ''; + } +} diff --git a/views/admin/landing.phtml b/views/admin/landing.phtml new file mode 100644 index 0000000..5be9f3c --- /dev/null +++ b/views/admin/landing.phtml @@ -0,0 +1,69 @@ + += htmlspecialchars($link['url']) ?>
+Status: = $link['enabled'] ? 'Enabled' : 'Disabled' ?>
+No social links found. Add your first social link.
+- A New Era of Chaos Begins + = htmlspecialchars($getContent('home', 'hero_subtitle')) ?>
- Dive into an epic multiverse where heroes unite against the forces of chaos. - From the depths of the Skibidi universe to the realms of Marvel, DC, Star Wars, and beyond. + = $getContent('home', 'hero_description') ?>