diff --git a/Answers to technical questions.md b/Answers to technical questions.md new file mode 100644 index 0000000..2e359d9 --- /dev/null +++ b/Answers to technical questions.md @@ -0,0 +1,50 @@ +# Answers to Technical Questions + +### 1. How long did you spend on the coding test? What would you add to your solution if you had more time? +* **Time Spent:** I spent approximately 2 and a half days working on this evaluation. This included setting up the MySQL database layer, creating a reliable PHP MVC folder architecture to handle slide records, implementing the administrative CRUD dashboards, and refining the frontend interface for pixel-perfect responsiveness matching both desktop and mobile target mockups. + +* **Production-Grade Features Already Implemented:** + 1. **Robust Validation Layer (Create & Edit):** Implemented thorough data sanitization using `trim()` alongside mandatory required string tracking. I also added strict browser-side validation blocks (`required` and explicit asset input `accept` parameters) to guide user entries. + 2. **Deep Binary Image Inspections:** Implemented server-side security using PHP's `finfo` extension to analyze the underlying binary magic bytes of file uploads, safely blocking malicious or corrupted injections from breaking layouts regardless of their renamed file extension. + 3. **Atomic File Swapping Operations:** Built a safe file-swapping pipeline during record adjustments that ensures older slide assets are *only* pruned from disk (`unlink()`) once the new image upload and MySQL database transactions succeed completely. If a transaction fails, it auto-rolls back file transfers to save space and prevent broken carousel links. + 4. **UX Destructive Safeguards:** Wired JavaScript confirmation dialog interceptors on all dashboard element removal controls to protect against unintended record purging. + 5. **Separation of Concerns:** Externalized frontend interactive JavaScript controllers into detached component assets (`main.js`), eliminating script parsing overhead from server-side markup trees. + 6. **De-coupled Asset Management:** Externalized all custom presentation layers into a dedicated stylesheet layout (`style.css`). This strict separation reduces template overhead, enables native browser asset caching, and radically maximizes Cumulative Layout Shift (CLS) performance scores. + +* **What I would add with more time:** + 1. **Architecture Upgrade (React & Framework Integration):** If given more time, I would refactor the front-end architecture of this module into a single, cohesive **React.js application**. Instead of server-side PHP loops rendering the static containers, I would create a highly optimized, state-driven reusable component layout. It would dynamically pull database slides through a lightweight **asynchronous Ajax API layer** hosted on an optimized **Laravel**, **CodeIgniter**, or **Yii** REST API controller endpoint. This structural separation would completely eliminate layout flickering, enable smooth fluid layout translations, and drastically decouple the front-end presentation from our core server layer. + +--- + +### 2. How would you track down a performance issue in production? Have you ever had to do this? +Yes, tracking down latency and system bottlenecks in production requires a highly structured, multi-tier diagnostic approach across the entire stack: + +* **Database Tier (PostgreSQL / MySQL):** I look for unoptimized queries, missing foreign key indexes, or heavy sequential table scans. In PostgreSQL, I run `EXPLAIN ANALYZE` on slower queries to track execution tree costs. In MySQL, I utilize the Slow Query Log and implement connection pooling to handle heavy user traffic efficiently. +* **Backend Application Tier (Laravel / CodeIgniter / Yii):** I profile execution timelines using specialized ecosystem tooling (e.g., Laravel Telescope, debug bars, or Xdebug profiling logs). Common bottlenecks often include "N+1 query problems" which I resolve using eager loading. I optimize performance by caching highly repetitive, static data queries using Redis or Memcached structures. +* **Frontend Tier (React.js / Ajax / Asset Pipelines):** For client-side latency, I audit network operations in Chrome DevTools to check asynchronous Ajax payload sizes. In React, I prevent unnecessary re-renders of complex visual structures by optimizing the component architecture with hooks like `useMemo`, `useCallback`, and implementing lazy loading for large code splittings. +* **Workflow & Error Isolation:** I track application issues through centralized error logging platforms and align bug resolutions systematically within our team's **Jira** sprint tracking boards. + +--- + +### 3. Please describe yourself using JSON. +```json +{ + "name": "Ashwini", + "role": "Full Stack Developer", + "professional_profile": { + "philosophy": "Detail-oriented engineer focused on writing highly optimized, clean, and reusable code structures following robust DRY and SOLID design principles.", + "methodologies": ["Agile/Scrum", "MVC Architecture", "Object-Oriented Programming (OOP)"] + }, + "technical_skills": { + "backend_frameworks": ["Laravel", "CodeIgniter", "Yii", "PHP (Core & OOP)"], + "frontend_ecosystem": ["React.js", "JavaScript (ES6+)", "jQuery", "Ajax", "Bootstrap 5", "HTML5/CSS3"], + "databases_and_storage": ["PostgreSQL", "MySQL"], + "version_control": ["Git", "SVN"], + "project_management_tools": ["JIRA"] + }, + "engineering_strengths": { + "optimization": "Database indexing, query profiling, caching strategies, and minifying asset payloads to enhance Core Web Vitals.", + "reusability": "Designing generic database service layers, building reusable React components, and abstracting shared utility helpers.", + "attention_to_detail": "Thorough edge-case testing, cross-browser CSS responsiveness, and strict adherence to Figma/design layout mockups." + } +} \ No newline at end of file diff --git a/app/config/config.php b/app/config/config.php new file mode 100644 index 0000000..d854310 --- /dev/null +++ b/app/config/config.php @@ -0,0 +1,19 @@ +connection = null; + + try { + + $this->connection = new PDO( + "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, + DB_USER, + DB_PASS + ); + + $this->connection->setAttribute( + PDO::ATTR_ERRMODE, + PDO::ERRMODE_EXCEPTION + ); + + } catch (PDOException $e) { + die("Connection failed: " . $e->getMessage()); + } + + return $this->connection; + } +} \ No newline at end of file diff --git a/app/controllers/SlideController.php b/app/controllers/SlideController.php new file mode 100644 index 0000000..fb0dd47 --- /dev/null +++ b/app/controllers/SlideController.php @@ -0,0 +1,40 @@ +slideModel = new Slide($db); + } + + // GET ALL SLIDES (for frontend or admin list) + public function index() + { + return $this->slideModel->getAllSlides(); + } + + // CREATE SLIDE + public function store($data) + { + return $this->slideModel->createSlide($data); + } + + public function getById($id) + { + return $this->slideModel->getSlideById($id); + } + + public function update($data) + { + return $this->slideModel->updateSlide($data); + } + + public function delete($id) + { + return $this->slideModel->deleteSlide($id); + } +} \ No newline at end of file diff --git a/app/models/slide.php b/app/models/slide.php new file mode 100644 index 0000000..ba9d0fe --- /dev/null +++ b/app/models/slide.php @@ -0,0 +1,115 @@ +connection = $database; + } + + // Get all slides + public function getAllSlides() + { + $query = "SELECT * FROM {$this->table} ORDER BY display_order ASC"; + + $statement = $this->connection->prepare($query); + + $statement->execute(); + + return $statement->fetchAll(PDO::FETCH_ASSOC); + } + + // Create slide + public function createSlide($data) + { + $query = "INSERT INTO {$this->table} + ( + tab_title, + tag_line, + slide_title, + description, + button_text, + button_link, + image, + display_order + ) + VALUES + ( + :tab_title, + :tag_line, + :slide_title, + :description, + :button_text, + :button_link, + :image, + :display_order + )"; + + $statement = $this->connection->prepare($query); + + return $statement->execute([ + + ':tab_title' => $data['tab_title'], + ':tag_line' => $data['tag_line'], + ':slide_title' => $data['slide_title'], + ':description' => $data['description'], + ':button_text' => $data['button_text'], + ':button_link' => $data['button_link'], + ':image' => $data['image'], + ':display_order' => $data['display_order'] + + ]); + } + + public function getSlideById($id) + { + $query = "SELECT * FROM slides WHERE id = :id"; + $stmt = $this->connection->prepare($query); + $stmt->bindParam(':id', $id); + $stmt->execute(); + + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function updateSlide($data) + { + $query = "UPDATE slides SET + tab_title = :tab_title, + tag_line = :tag_line, + slide_title = :slide_title, + description = :description, + button_text = :button_text, + button_link = :button_link, + image = :image, + display_order = :display_order + WHERE id = :id"; + + $stmt = $this->connection->prepare($query); + + return $stmt->execute([ + ':tab_title' => $data['tab_title'], + ':tag_line' => $data['tag_line'], + ':slide_title' => $data['slide_title'], + ':description' => $data['description'], + ':button_text' => $data['button_text'], + ':button_link' => $data['button_link'], + ':image' => $data['image'], + ':display_order' => $data['display_order'], + ':id' => $data['id'] + ]); + } + + public function deleteSlide($id) + { + $query = "DELETE FROM slides WHERE id = :id"; + + $stmt = $this->connection->prepare($query); + + return $stmt->execute([ + ':id' => $id + ]); + } +} \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..2442b93 --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,357 @@ + +body { + margin: 0; + background: #072b4a; /* Matched to design dark blue */ + font-family: Arial, sans-serif; +} + +/* ==================================== +MAIN SECTION +==================================== */ +.main-section { + padding: 60px 0; +} + +.main-heading { + text-align: center; + color: #fff; + margin-bottom: 45px; +} + +.main-heading h1 { + font-size: 42px; + font-weight: normal; + margin-bottom: 15px; +} + +.main-heading p { + font-size: 16px; + color: #b0c4de; + max-width: 600px; + margin: 0 auto; +} + +.custom-container { + width: 100%; + max-width: 1140px; + margin: 0 auto; + padding: 0 15px; +} + +/* ==================================== +DESKTOP SYSTEM (Widths >= 992px) +==================================== */ +.desktop-view { + display: block; +} + +.mobile-view { + display: none; +} + +.main-wrapper { + display: flex; + height: 520px; + background: #fff; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + overflow: hidden; +} + +/* LEFT TABS */ +.tab-wrapper { + width: 28%; + background: #f4f5f7; + padding: 25px 15px; + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 5px; +} + +.tab-item { + background: #fff; + padding: 18px 20px; + margin-bottom: 15px; + font-size: 20px; + font-weight: 600; + color: #333; + cursor: pointer; + position: relative; + transition: 0.3s; + border-radius: 4px; + display: flex; + align-items: center; + gap: 15px; + box-shadow: 0 2px 4px rgba(0,0,0,0.02); +} + +.tab-item.active { + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.tab-item.active::after { + content: ''; + position: absolute; + right: -15px; + top: 50%; + transform: translateY(-50%); + border-left: 15px solid #74bfd0; + border-top: 15px solid transparent; + border-bottom: 15px solid transparent; + z-index: 10; +} + +.tab-icon { + width: 32px; + height: 32px; + object-fit: contain; +} + +.tab-text { + flex-grow: 1; +} + +/* TEAL CENTER SLIDER */ +.content-wrapper { + width: 37%; + background: #74bfd0; + height: 100%; +} + +.content-slider { + height: 100%; +} + +.content-slider .slick-list, +.content-slider .slick-track, +.content-slider .slick-slide > div { + height: 100%; +} + +.slide-box { + height: 100%; + /* Increase padding to match design, but keep it tight at the top */ + padding: 50px 40px; + display: flex; + flex-direction: column; + /* CHANGE THIS FROM center TO flex-start */ + justify-content: flex-start; + color: #fff; +} + +.slide-box h5 { + font-size: 11px; + text-transform: uppercase; + margin-bottom: 20px; + background: rgba(0, 0, 0, 0.15); + align-self: flex-start; + padding: 5px 12px; + letter-spacing: 1px; + font-weight: bold; + border-radius: 2px; +} + +.slide-box h2 { + font-size: 32px; + line-height: 1.3; + margin-bottom: 25px; + font-weight: normal; +} + +.slide-box .learn-more-link { + color: #fff; + text-decoration: none; + font-size: 16px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 8px; +} + +/* RIGHT FRAMED IMAGE SLIDER */ +.image-wrapper { + width: 35%; + height: 100%; + background: #000; +} + +.image-slider { + height: 100%; +} + +.image-slider .slick-list, +.image-slider .slick-track, +.image-slider .slick-slide > div { + height: 100%; +} + +.image-wrapper img { + width: 100%; + height: 100% !important; + object-fit: cover; + display: block; +} + +.slick-dots { + bottom: 25px !important; +} +.slick-dots li button:before { + color: #fff !important; + opacity: 0.4; + font-size: 10px; +} +.slick-dots li.slick-active button:before { + color: #fff !important; + opacity: 1; +} + +/* ==================================== +RESPONSIVE REFACTOR (Under 991px Viewport) +==================================== */ +@media (max-width: 991px) { + .desktop-view { + display: none !important; + } + + .mobile-view { + display: block !important; + } + + .main-section { + padding: 40px 0; + } + + .main-heading h1 { + font-size: 32px; + } + + .mobile-card { + margin-bottom: 16px; + background: #fff; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0,0,0,0.15); + } + + .mobile-tab { + padding: 16px 20px; + cursor: pointer; + background: #fff; + display: flex; + align-items: center; + justify-content: space-between; + } + + .mobile-tab-left { + display: flex; + align-items: center; + gap: 15px; + } + + .mobile-tab-left img { + width: 35px; + height: 35px; + object-fit: contain; + } + + .mobile-title { + font-size: 20px; + font-weight: 600; + color: #333; + margin: 0; + } + + /* Icon box container sizing */ + .mobile-icon { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .mobile-icon img { + width: 100%; + height: 100%; + object-fit: contain; + } + + .mobile-content { + position: relative; + } + + /* Teal Top pointer triangle block */ + .mobile-content::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + border-bottom: 10px solid #74bfd0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + z-index: 5; + } + + /* Refactored layout with background fallbacks */ + .mobile-slide { + /* Adjusted top padding to 40px to start text cleanly without a massive drop gap */ + padding: 40px 25px 55px 25px; + text-align: center; + color: #fff; + display: flex; + flex-direction: column; + align-items: center; + /* CHANGE THIS FROM center TO flex-start */ + justify-content: flex-start; + min-height: 340px; + position: relative; + z-index: 1; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + + /* Pure overlay layer to guarantee image visibility blends smoothly */ + .mobile-slide::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(116, 191, 208, 0.92); /* Matches your design layout overlay tints */ + z-index: -1; + } + + .mobile-slide h5 { + font-size: 11px; + letter-spacing: 1px; + margin-bottom: 15px; + text-transform: uppercase; + background: rgba(0, 0, 0, 0.15); + padding: 5px 12px; + font-weight: bold; + display: inline-block; + border-radius: 2px; + } + + .mobile-slide h2 { + font-size: 24px; + line-height: 1.4; + margin-bottom: 20px; + font-weight: normal; + max-width: 90%; + } + + .mobile-slide .learn-more-link { + color: #fff; + text-decoration: none; + font-size: 16px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 6px; + } +} diff --git a/assets/images/DL-communication.svg b/assets/images/DL-communication.svg new file mode 100644 index 0000000..0088544 --- /dev/null +++ b/assets/images/DL-communication.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/DL-learning.svg b/assets/images/DL-learning.svg new file mode 100644 index 0000000..bfeb0b5 --- /dev/null +++ b/assets/images/DL-learning.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/DL-technology.svg b/assets/images/DL-technology.svg new file mode 100644 index 0000000..59f2ea5 --- /dev/null +++ b/assets/images/DL-technology.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/arrow-right.svg b/assets/images/arrow-right.svg new file mode 100644 index 0000000..37d307b --- /dev/null +++ b/assets/images/arrow-right.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/assets/images/minus-01.svg b/assets/images/minus-01.svg new file mode 100644 index 0000000..a0639ca --- /dev/null +++ b/assets/images/minus-01.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/assets/images/plus-01.svg b/assets/images/plus-01.svg new file mode 100644 index 0000000..d80a766 --- /dev/null +++ b/assets/images/plus-01.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..535411f --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,64 @@ +$(document).ready(function(){ + // Configuration object to avoid repeating settings + const slickOptions = { + slidesToShow: 1, + slidesToScroll: 1, + arrows: false, + fade: true + }; + + /* Initialize Content Slider */ + $('.content-slider').slick({ + ...slickOptions, + dots: true, + asNavFor: '.image-slider' + }); + + /* Initialize Image Slider */ + $('.image-slider').slick({ + ...slickOptions, + asNavFor: '.content-slider' + }); + + /* Sync Left Tabs with Slick Slide Shifts */ + $('.tab-item').click(function(){ + const slideIndex = $(this).data('slide'); + $('.content-slider').slick('slickGoTo', parseInt(slideIndex)); + }); + + /* Active Tab Class Management on Slide Change */ + $('.content-slider').on('afterChange', function(event, slick, currentSlide){ + $('.tab-item').removeClass('active'); + $('.tab-item').extra = $(`.tab-item[data-slide="${currentSlide}"]`).addClass('active'); + }); + + /* Mobile Accordion Execution Logic */ + $('.mobile-tab').click(function(){ + const $this = $(this); + const parent = $this.closest('.mobile-card'); + const contentPanel = parent.find('.mobile-content'); + + // Target specific icons + const currentIconContainer = $this.find('.mobile-icon'); + const plusIcon = currentIconContainer.data('plus-src'); + const minusIcon = currentIconContainer.data('minus-src'); + + // If clicked item is already open, close it + if (contentPanel.is(':visible')) { + contentPanel.slideUp(); + currentIconContainer.find('img').attr('src', plusIcon); + return; + } + + // Close all other open cards completely and reset their icon graphics to a plus sign + $('.mobile-content').slideUp(); + $('.mobile-icon').each(function() { + const $iconBox = $(this); + $iconBox.find('img').attr('src', $iconBox.data('plus-src')); + }); + + // Open current targeted slide smoothly and switch icon to a minus sign + contentPanel.slideDown(); + currentIconContainer.find('img').attr('src', minusIcon); + }); +}); \ No newline at end of file diff --git a/database/database.sql b/database/database.sql new file mode 100644 index 0000000..84a3a1b --- /dev/null +++ b/database/database.sql @@ -0,0 +1,23 @@ +CREATE TABLE slides ( + + id INT AUTO_INCREMENT PRIMARY KEY, + + tab_title VARCHAR(255) NOT NULL, + + tag_line VARCHAR(255) NOT NULL, + + slide_title VARCHAR(255) NOT NULL, + + description TEXT NOT NULL, + + button_text VARCHAR(100) NOT NULL, + + button_link VARCHAR(255) NOT NULL, + + image VARCHAR(255) NOT NULL, + + display_order INT DEFAULT 0, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +); \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..cd8bc6f --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,10 @@ +RewriteEngine On + +# Force remove trailing slash +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.+)/$ /$1 [R=301,L] + +# Route /admin → /admin/ +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^admin$ admin/index.php [L] \ No newline at end of file diff --git a/public/admin/create.php b/public/admin/create.php new file mode 100644 index 0000000..62f3928 --- /dev/null +++ b/public/admin/create.php @@ -0,0 +1,159 @@ +connect(); + +$controller = new SlideController($db); + +$message = ""; + +/* ========================= + FORM SUBMIT LOGIC +========================= */ +if (isset($_POST['submit'])) { + + $errors = []; + $imageName = ""; + + // 1. DATA SANITIZATION & EMPTY FIELDS VALIDATION + $tab_title = trim($_POST['tab_title'] ?? ''); + $tag_line = trim($_POST['tag_line'] ?? ''); + $slide_title = trim($_POST['slide_title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $button_text = trim($_POST['button_text'] ?? ''); + $button_link = trim($_POST['button_link'] ?? ''); + $display_order = trim($_POST['display_order'] ?? 0); + + if (empty($tab_title)) { $errors[] = "Tab Title is required."; } + if (empty($tag_line)) { $errors[] = "Tag Line is required."; } + if (empty($slide_title)) { $errors[] = "Slide Title is required."; } + + // 2. SECURE IMAGE UPLOAD VALIDATION + if (!empty($_FILES['image']['name'])) { + + // Check for internal upload errors + if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) { + $errors[] = "An error occurred during the image upload."; + } else { + // Validate file type (MIME Type) to ensure it's an image + $fileTmpPath = $_FILES['image']['tmp_name']; + $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/svg+xml']; + + // Using finfo to check actual file content instead of just the extension + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->file($fileTmpPath); + + if (!in_array($mimeType, $allowedMimeTypes)) { + $errors[] = "Invalid file format. Only JPG, PNG, and SVG formats are permitted."; + } + + // Validate file size (optional, e.g., max 3MB) + $maxFileSize = 3 * 1024 * 1024; // 3 Megabytes + if ($_FILES['image']['size'] > $maxFileSize) { + $errors[] = "The uploaded image exceeds the maximum permitted size of 3MB."; + } + } + + // If no file errors occurred, generate a sanitized filename and prepare upload path + if (empty($errors)) { + // Clean up original file name to avoid path traversal vulnerabilities + $cleanOriginalName = preg_replace("/[^a-zA-Z0-9\._-]/", "", $_FILES['image']['name']); + $imageName = time() . '_' . $cleanOriginalName; + $targetPath = UPLOAD_PATH . $imageName; + } + } else { + $errors[] = "Please upload a slide image asset."; + } + + // 3. EXECUTION OR ERROR HANDLING + if (empty($errors)) { + + $data = [ + 'tab_title' => $tab_title, + 'tag_line' => $tag_line, + 'slide_title' => $slide_title, + 'description' => $description, + 'button_text' => $button_text, + 'button_link' => $button_link, + 'image' => $imageName, + 'display_order' => intval($display_order) // Force integer data type + ]; + + // Perform final file relocation safely right before database insert + if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) { + $result = $controller->store($data); + + if ($result) { + header("Location: index.php"); + exit; + } else { + // If DB storage fails, clean up the newly uploaded file to avoid dangling assets + if (file_exists($targetPath)) { + unlink($targetPath); + } + $message = "Failed to write slide entry into the database."; + } + } else { + $message = "File system restriction: Could not transfer uploaded image to the storage directory."; + } + } else { + // Flatten array arrays to build a visual notification block for the dashboard view + $message = implode("
", $errors); + } +} + +?> + + + + + + Add Slide + + + + +
+ +

Add New Slide

+ + + + + +
+ + + + + + + + + + + + + + + + + + + + Back + +
+ +
+ + + \ No newline at end of file diff --git a/public/admin/delete.php b/public/admin/delete.php new file mode 100644 index 0000000..7dfc4c4 --- /dev/null +++ b/public/admin/delete.php @@ -0,0 +1,63 @@ +connect(); + +$controller = new SlideController($db); + +/* +|-------------------------------------------------------------------------- +| GET ID +|-------------------------------------------------------------------------- +*/ + +$id = $_GET['id'] ?? null; + +if (!$id) { + header("Location: index.php"); + exit; +} + +/* +|-------------------------------------------------------------------------- +| GET SINGLE RECORD +|-------------------------------------------------------------------------- +*/ + +$slide = $controller->getById($id); + +if (!$slide) { + header("Location: index.php"); + exit; +} + +/* +|-------------------------------------------------------------------------- +| DELETE IMAGE FROM FOLDER +|-------------------------------------------------------------------------- +*/ + +$imagePath = "../../uploads/" . $slide['image']; + +if (!empty($slide['image']) && file_exists($imagePath)) { + unlink($imagePath); +} + +/* +|-------------------------------------------------------------------------- +| DELETE FROM DATABASE +|-------------------------------------------------------------------------- +*/ + +$result = $controller->delete($id); + +if ($result) { + header("Location: index.php"); + exit; +} else { + echo "Failed to delete record"; +} \ No newline at end of file diff --git a/public/admin/edit.php b/public/admin/edit.php new file mode 100644 index 0000000..0813c8b --- /dev/null +++ b/public/admin/edit.php @@ -0,0 +1,292 @@ +connect(); + +$controller = new SlideController($db); + +/* +|-------------------------------------------------------------------------- +| GET ID FROM URL +|-------------------------------------------------------------------------- +*/ + +$id = $_GET['id'] ?? null; + +if (!$id) { + + header("Location: index.php"); + exit; +} + +/* +|-------------------------------------------------------------------------- +| FETCH EXISTING SLIDE DATA +|-------------------------------------------------------------------------- +*/ + +$slide = $controller->getById($id); + +/* +|-------------------------------------------------------------------------- +| UPDATE FORM SUBMIT +|-------------------------------------------------------------------------- +*/ + +if (isset($_POST['update'])) { + + $errors = []; + $imageName = $slide['image']; // Default to keeping the old image name + + // 1. DATA SANITIZATION & EMPTY REQUIRED FIELDS VALIDATION + $tab_title = trim($_POST['tab_title'] ?? ''); + $tag_line = trim($_POST['tag_line'] ?? ''); + $slide_title = trim($_POST['slide_title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $button_text = trim($_POST['button_text'] ?? ''); + $button_link = trim($_POST['button_link'] ?? ''); + $display_order = trim($_POST['display_order'] ?? 0); + + if (empty($tab_title)) { $errors[] = "Tab Title cannot be left empty."; } + if (empty($tag_line)) { $errors[] = "Tag Line cannot be left empty."; } + if (empty($slide_title)) { $errors[] = "Slide Title cannot be left empty."; } + + // 2. OPTIONAL NEW IMAGE UPLOAD SECURITY CHECK + $newImageUploaded = false; + if (!empty($_FILES['image']['name'])) { + + // Validate internal upload parameters + if ($_FILES['image']['error'] !== UPLOAD_ERR_OK) { + $errors[] = "An error occurred during the new image upload loop."; + } else { + // Confirm the underlying binary structure is a safe graphic format + $fileTmpPath = $_FILES['image']['tmp_name']; + $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/svg+xml']; + + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->file($fileTmpPath); + + if (!in_array($mimeType, $allowedMimeTypes)) { + $errors[] = "Invalid file format. Only JPG, PNG, and SVG elements are permitted."; + } + + // Size Constraint Enforcement (Max 3MB) + $maxFileSize = 3 * 1024 * 1024; + if ($_FILES['image']['size'] > $maxFileSize) { + $errors[] = "The uploaded file exceeds the 3MB storage configuration limit."; + } + } + + // Prepare file details if no errors occurred + if (empty($errors)) { + $cleanOriginalName = preg_replace("/[^a-zA-Z0-9\._-]/", "", $_FILES['image']['name']); + $imageName = time() . '_' . $cleanOriginalName; + $targetPath = UPLOAD_PATH . $imageName; + $newImageUploaded = true; + } + } + + // 3. DATABASE TRANSACTION & ATOMIC FILE SWAPPING + if (empty($errors)) { + + $data = [ + 'id' => $id, + 'tab_title' => $tab_title, + 'tag_line' => $tag_line, + 'slide_title' => $slide_title, + 'description' => $description, + 'button_text' => $button_text, + 'button_link' => $button_link, + 'image' => $imageName, + 'display_order' => intval($display_order) // Hard-cast to dynamic integer properties + ]; + + // Process file move if a brand new file payload was delivered + if ($newImageUploaded) { + if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) { + + // Execute database update layer via Controller + $result = $controller->update($data); + + if ($result) { + // CRUCIAL: Only delete the older asset if the new file is saved and DB transaction succeeds + $oldImagePath = UPLOAD_PATH . $slide['image']; + if (!empty($slide['image']) && file_exists($oldImagePath)) { + unlink($oldImagePath); + } + + header("Location: index.php"); + exit; + } else { + // Fallback cleanup if query fails: remove dangling upload to save server space + if (file_exists($targetPath)) { + unlink($targetPath); + } + $message = "Database Error: Could not overwrite slide properties record."; + } + } else { + $message = "File system restriction: Write access denied on uploads directory destination."; + } + } else { + // Simple plain update routine execution when retaining original graphic structures + $result = $controller->update($data); + if ($result) { + header("Location: index.php"); + exit; + } else { + $message = "Database Error: Slide record values could not be transformed."; + } + } + } else { + // Flatten array parameters out to pass to your Bootstrap alerting block placeholder + $message = implode("
", $errors); + } +} +?> + + + + + + + + Edit Slide + + + + + + + +
+ +

Edit Slide

+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Current Slide Graphic +
+ +
+ + +
Leave empty to retain the current image. Only JPG, PNG, and SVG are supported.
+
+ +
+ + + + Back + +
+ +
+
+ + + \ No newline at end of file diff --git a/public/admin/index.php b/public/admin/index.php new file mode 100644 index 0000000..e463d6a --- /dev/null +++ b/public/admin/index.php @@ -0,0 +1,67 @@ +connect(); + +$controller = new SlideController($db); +$slides = $controller->index(); + +?> + + + + + Admin - Slides + + + + + + +
+ +
+

Slides

+ + Add Slide +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDTab TitleSlide TitleImageAction
+ + + Edit + + Delete + +
+
+ + \ No newline at end of file diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..a89086a --- /dev/null +++ b/public/index.php @@ -0,0 +1,144 @@ +connect(); + +$controller = new SlideController($db); + +$slides = $controller->index(); + +/* +GROUP TABS +*/ +$tabs = []; +foreach ($slides as $index => $slide) { + if (!isset($tabs[$slide['tab_title']])) { + $tabs[$slide['tab_title']] = $index; + } +} + +?> + + + + + + WPoets Frontend + + + + + + + + +
+
+ +
+

DelphianLogic in Action

+

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo

+
+ +
+
+ +
+ $index): + $icon_filename = 'DL-learning.svg'; + if (strpos(strtolower($tab), 'tech') !== false) { + $icon_filename = 'DL-technology.svg'; + } elseif (strpos(strtolower($tab), 'comm') !== false) { + $icon_filename = 'DL-communication.svg'; + } + ?> +
+ Tab Icon + +
+ +
+ +
+
+ +
+
+
+

+ + → + +
+
+ +
+
+ +
+
+ +
+ Slide Production Showcase +
+ +
+
+ +
+
+ +
+ $slide): + $mobile_icon_filename = 'DL-learning.svg'; + if (strpos(strtolower($slide['tab_title']), 'tech') !== false) { + $mobile_icon_filename = 'DL-technology.svg'; + } elseif (strpos(strtolower($slide['tab_title']), 'comm') !== false) { + $mobile_icon_filename = 'DL-communication.svg'; + } + ?> +
+
+
+ Category Icon +

+
+ +
+ Toggle State Indicator +
+
+ +
> +
+
+

+ + → + +
+
+
+ +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/public/test.php b/public/test.php new file mode 100644 index 0000000..7f82637 --- /dev/null +++ b/public/test.php @@ -0,0 +1,11 @@ +connect(); + +if ($connection) { + echo "Database connected successfully"; +} \ No newline at end of file diff --git a/uploads/1780142052_DL-Learning-1.jpg b/uploads/1780142052_DL-Learning-1.jpg new file mode 100644 index 0000000..755171f Binary files /dev/null and b/uploads/1780142052_DL-Learning-1.jpg differ diff --git a/uploads/1780142066_DL-Technology.jpg b/uploads/1780142066_DL-Technology.jpg new file mode 100644 index 0000000..a8d4354 Binary files /dev/null and b/uploads/1780142066_DL-Technology.jpg differ diff --git a/uploads/1780143115_DL-Communication.jpg b/uploads/1780143115_DL-Communication.jpg new file mode 100644 index 0000000..51b3b5b Binary files /dev/null and b/uploads/1780143115_DL-Communication.jpg differ