355 Commits

Author SHA1 Message Date
Ümit Tunç d0292af20c feat: add GitHub username field and implement commit filtering by author in intern dashboard 2026-07-01 23:46:12 +03:00
Ümit Tunç 26670fcca5 feat: implement internship journal approval status tracking and add single-day printing functionality 2026-07-01 23:36:26 +03:00
Ümit Tunç f557d4b91c feat: replace eager in-memory GitHub commit fetching with on-demand retrieval and add a fallback UI for missing commits. 2026-07-01 23:34:42 +03:00
Ümit Tunç 09e92506fd feat: implement tabbed interface to toggle between Gantt chart and intern list in admin dashboard 2026-07-01 23:15:36 +03:00
Ümit Tunç 129dae4570 feat: add journal progress tracking UI for internship applications in the career dashboard 2026-07-01 23:06:13 +03:00
Ümit Tunç f0a9180a54 feat: add journal detail tracking, progress reporting, and notebook signing functionality for intern management 2026-07-01 23:04:23 +03:00
Ümit Tunç a6c250c13e refactor: simplify internship approval process by removing unit approval and adding supervisor name tracking 2026-07-01 22:37:08 +03:00
Ümit Tunç f84f78a138 feat: add approval system for internship journal entries including database fields, admin toggle functionality, and rich-text rendering 2026-07-01 21:58:56 +03:00
Ümit Tunç d9c1c825d9 style: increase container maximum width to 1440px in intern dashboard view 2026-07-01 17:49:06 +03:00
Ümit Tunç 13f8f09524 refactor: move tab logic and implement Quill editor and GitHub commit integration in scratch_test.js 2026-07-01 17:46:57 +03:00
Ümit Tunç 351b9064af feat: integrate Quill editor for internship journal entries, add retroactive field support, and enforce future date restrictions 2026-07-01 17:43:22 +03:00
Ümit Tunç edc843fa1c feat: restrict verification page view to accepted applications only 2026-07-01 17:40:06 +03:00
Ümit Tunç 8cf9e0b191 style: update career portal branding, logo assets, and dashboard text visibility 2026-07-01 17:25:52 +03:00
Ümit Tunç 44852bdc95 feat: add download icon and text to signed internship form button in intern dashboard 2026-07-01 17:21:39 +03:00
Ümit Tunç 7b70621c5a feat: implement interactive intern dashboard journal features and correct resource component namespace 2026-07-01 17:20:19 +03:00
Ümit Tunç 4c5cf1d355 feat: implement internship journal entry system with printable reports and approval workflow 2026-07-01 17:16:41 +03:00
Ümit Tunç 5b46b4ac9e refactor: restrict intern dashboard updates to pending status and automate username assignment from email 2026-07-01 17:06:11 +03:00
Ümit Tunç 32c556a87b feat: add internship workflow guide modal to dashboards and visualize current date in Gantt chart 2026-06-26 16:42:24 +03:00
Ümit Tunç aa6858bc0f chore: reorder DevExtreme script dependencies in gantt partial 2026-06-26 16:01:17 +03:00
Ümit Tunç 04e62ea9b8 refactor: modularize admin dashboard by extracting components into separate partial views 2026-06-26 16:00:10 +03:00
Ümit Tunç 66366aa8b8 refactor: modularize admin dashboard view with partials and update intern parent ID mapping for gantt chart rendering 2026-06-26 15:59:59 +03:00
Ümit Tunç 0e558fd1dc feat: implement intern admin portal with login and dashboard management functionality 2026-06-26 15:54:21 +03:00
Ümit Tunç 01be250e6c feat: add summer background images to assets directory 2026-06-25 16:19:07 +03:00
Ümit Tunç 6614d13be2 refactor: update service cards to full-width mobile layout and apply light theme glass styling 2026-06-23 11:24:06 +03:00
Ümit Tunç 61480399b3 refactor: wrap service cards in anchor tags and update styling for a glassmorphism effect 2026-06-23 11:23:15 +03:00
Ümit Tunç e3e7ac7f18 style: add mobile-specific background image styling to hero section 2026-06-23 11:10:28 +03:00
Ümit Tunç f1c7e57e43 style: refine services card responsive typography and layout styling 2026-06-23 11:06:06 +03:00
Ümit Tunç ad50102c42 feat: add localized routes for privacy policy pages 2026-06-23 10:57:52 +03:00
Ümit Tunç 007da1227d fix: update intern gantt chart scale type from weeks to days 2026-06-16 17:26:32 +03:00
Ümit Tunç 58ae406b36 feat: replace custom gantt chart implementation with DevExtreme widget 2026-06-16 17:14:54 +03:00
Ümit Tunç 794f9556de feat: implement custom responsive gantt chart UI for intern applications widget 2026-06-16 15:28:20 +03:00
Ümit Tunç 6be7f4442a feat: add Gantt chart widget to visualize accepted intern schedules in the admin panel 2026-06-16 15:27:28 +03:00
Ümit Tunç 9361a01c80 feat: update intern dashboard to display signed status and prevent modifications for approved internship forms 2026-06-10 21:02:03 +03:00
Ümit Tunç f9abe3c02a feat: add visual progress timeline to the intern application dashboard 2026-06-10 20:47:57 +03:00
Ümit Tunç efc6b911e7 fix: clear password field state in intern application resource form 2026-06-10 20:44:09 +03:00
Ümit Tunç 793a843be7 fix: remove authorized signature section from application verification view 2026-06-08 19:39:03 +03:00
Ümit Tunç 742303eb73 refactor: remove decorative borders and normalize print layout styling for PDF generation 2026-06-08 19:35:08 +03:00
Ümit Tunç cfce7b8b3f style: convert certificate text elements to uppercase and remove redundant classes 2026-06-08 19:25:34 +03:00
Ümit Tunç 13d12e4f8f style: update career certificate typography with new Cinzel font class and layout adjustments 2026-06-08 19:19:16 +03:00
Ümit Tunç ba6431e500 refactor: remove print button and update decorative underline styling in career verification page 2026-06-08 19:19:11 +03:00
Ümit Tunç eca57bf9cc style: add text-glow-white utility class and update career verification UI typography colors 2026-06-08 19:15:46 +03:00
Ümit Tunç 89b81e6ee8 style: refine verification banner and button UI for improved layout and spacing 2026-06-08 18:53:25 +03:00
Ümit Tunç fa06ba7545 feat: update certificate design with watermark logo, adjusted typography, and improved header layout 2026-06-08 18:51:15 +03:00
Ümit Tunç 4eb0a96832 feat: redesign certificate verification page with new futuristic background asset 2026-06-08 18:46:15 +03:00
Ümit Tunç 3600cc2334 refactor: implement global CSS variable for transcript typography scaling and apply semantic class-based sizing 2026-06-08 18:33:25 +03:00
Ümit Tunç 554f22695d refactor: optimize internship transcript layout and typography for A4 print consistency 2026-06-08 18:32:07 +03:00
Ümit Tunç af82f47ea0 feat: increase verification QR code sizes and adjust PDF image scaling logic 2026-06-08 18:27:34 +03:00
Ümit Tunç 612a24042b refactor: redesign internship verification page with updated layout, enhanced PDF download capabilities, and refined certificate styling 2026-06-08 18:22:39 +03:00
Ümit Tunç 082fb33af3 refactor: migrate career applications to separate job and intern Filament resources 2026-06-08 18:17:56 +03:00
Ümit Tunç 5b07f385b1 refactor: split CareerApplication into separate Job and Intern application resources in Filament and update hero social links 2026-06-08 18:17:51 +03:00
Ümit Tunç aa4a4b3e27 refactor: redesign intern journal timeline component with modernized animations and styles 2026-06-06 01:36:11 +03:00
Ümit Tunç b776ebbd26 refactor: replace inline journal timeline view with a dedicated Livewire component for improved performance and structure 2026-06-06 01:30:15 +03:00
Ümit Tunç 0d3a514456 refactor: organize career application form into tabs and add intern journal timeline component 2026-06-06 01:19:26 +03:00
Ümit Tunç 99e97edfd1 feat: implement carousel-based timeline view for internship dashboard commit history 2026-06-06 01:10:17 +03:00
Ümit Tunç db60909348 refactor: group internship commits by date and update UI to a grid layout 2026-06-06 01:10:13 +03:00
Ümit Tunç 080c39ac88 feat: add github_repo field to career applications and update intern dashboard UI 2026-06-06 01:05:29 +03:00
Ümit Tunç c3f343ad70 feat: add multilingual support to bank accounts and legal information sections 2026-06-02 22:11:42 +03:00
Ümit Tunç fb1224f7c9 feat: add PDF download functionality with loading state and improve print layout styling 2026-06-02 21:26:50 +03:00
Ümit Tunç f68bc87031 style: adjust company info margin to fix letterhead layout positioning 2026-06-02 20:19:07 +03:00
Ümit Tunç fee4c8fa78 style: remove background color from bank account statement description box 2026-06-02 20:17:38 +03:00
Ümit Tunç e8352f599b refactor: modernize bank account page layout with premium tab styling and optimized letterhead print view 2026-06-02 20:16:35 +03:00
Ümit Tunç 80de8dc6f9 feat: implement imprint page with copy-to-clipboard functionality and update corporate templates 2026-06-02 20:06:24 +03:00
Ümit Tunç e4e7599cb2 refactor: extract legal info and letterhead sections into separate partial blade templates 2026-06-02 19:58:15 +03:00
Ümit Tunç 88c1657b6a style: adjust top margin of company info block in bank accounts template 2026-06-02 19:55:26 +03:00
Ümit Tunç f195d22342 feat: implement A4 letterhead preview and print functionality for bank account details 2026-06-02 19:54:39 +03:00
Ümit Tunç 864f80c7b8 style: remove fixed background attachment from hero section 2026-06-02 19:52:38 +03:00
Ümit Tunç d245055bf4 feat: add automated WebP conversion and resizing for hero background images 2026-06-02 06:44:57 +03:00
Ümit Tunç 9c4dc0ff11 style: add hover transition effects to glass card headings 2026-06-02 06:29:17 +03:00
Ümit Tunç 092eb09316 fix: correct typo in CSS transition property and adjust hover brightness filter for service cards 2026-06-02 06:07:34 +03:00
Ümit Tunç 82ea32cd33 feat: add hover brightness transition effect to glass card icons 2026-06-02 06:07:28 +03:00
Ümit Tunç 2ee018c37c feat: apply glassmorphism effect to service cards and align call-to-action buttons 2026-06-02 06:01:59 +03:00
Ümit Tunç 81690db06d feat: make hero background image configurable via settings service 2026-06-02 05:59:43 +03:00
Ümit Tunç a9c872e5b4 refactor: remove redundant call-to-action section from web development template 2026-06-02 05:47:30 +03:00
Ümit Tunç 97e95efb17 refactor: remove all social media link components from header, navbar, and partials view files 2026-06-02 05:47:26 +03:00
Ümit Tunç 181ec53ef0 feat: update internship process workflow and localization labels in mermaid diagrams 2026-06-02 00:00:24 +03:00
Ümit Tunç 0a3feba23a feat: update internship process workflow and localization keys in career module 2026-06-02 00:00:20 +03:00
Ümit Tunç f099d8f46a refactor: update internship application requirements and refine the internal admission flow diagram 2026-06-01 23:46:47 +03:00
Ümit Tunç 36ec1823c8 feat: implement internship application wizard with terms acceptance and multi-step form view 2026-06-01 23:43:59 +03:00
Ümit Tunç 23f24e426b fix: adjust mermaid text styling and defer initialization until fonts are fully loaded to ensure correct layout rendering 2026-06-01 23:39:39 +03:00
Ümit Tunç cf4572cb03 feat: add fullscreen toggle functionality to the Mermaid flowchart card 2026-06-01 23:38:14 +03:00
Ümit Tunç efa575a920 style: update text color of decisionNode in career page mermaid diagram 2026-06-01 23:36:47 +03:00
Ümit Tunç fc2c99f0c2 style: improve readability and visual contrast for career page Mermaid diagrams 2026-06-01 23:36:43 +03:00
Ümit Tunç 3235a700c9 feat: add career application wizard and HR process documentation strings and dynamic views 2026-06-01 23:36:16 +03:00
Ümit Tunç e23ac8c346 feat: add ID photocopy upload field to career application form with validation and database storage 2026-06-01 23:32:52 +03:00
Ümit Tunç 93e45655eb feat: implement multi-step wizard interface for career application form 2026-06-01 23:30:39 +03:00
Ümit Tunç 1b0800beb6 chore: update default testimonial section image path 2026-06-01 23:29:03 +03:00
Ümit Tunç d828df82bb refactor: localize all UI strings in the dijital-donusum product page using the t() helper 2026-06-01 23:20:37 +03:00
Ümit Tunç 690ee11336 docs: update customer testimonial names and company affiliations in digital transformation product page 2026-06-01 23:18:45 +03:00
Ümit Tunç c719e58d4e chore: update partner section header text to localized string 2026-06-01 23:15:59 +03:00
Ümit Tunç 7e2cd6eff4 feat: dynamically render partner logos from CMS in digital transformation product page 2026-06-01 23:15:54 +03:00
Ümit Tunç 0bdf0c3394 feat: add digital transformation product page layout and styles 2026-06-01 23:09:46 +03:00
Ümit Tunç d7200111e2 feat: add digital transformation landing page and link in music production view 2026-06-01 23:09:42 +03:00
Ümit Tunç faf752dd3c feat: redesign music production metadata table with localized labels and styled UI components 2026-06-01 09:16:30 +03:00
Ümit Tunç 31e695cd3a feat: add metadata table component for music production details and update localization labels 2026-06-01 09:02:02 +03:00
Ümit Tunç f77866faa5 refactor: parse track titles for styles, update date formatting, and adjust UI transition animations in music production views 2026-06-01 08:53:46 +03:00
Ümit Tunç fbbed52a3d style: adjust music player hover transformation offsets and scale factor 2026-06-01 08:46:50 +03:00
Ümit Tunç e7ad0123ac refactor: update music production card layout and hover interaction styles 2026-06-01 08:46:21 +03:00
Ümit Tunç f34fc0f2a0 refactor: replace YouTube lightbox with persistent background playback integration and vinyl animation state management 2026-06-01 08:43:16 +03:00
Ümit Tunç 9e6bcc22b6 feat: replace CD mockup with animated vinyl player component in music productions list and detail views 2026-06-01 08:38:21 +03:00
Ümit Tunç 3c023ba588 style: remove backdrop-filter blur from music production overlay 2026-06-01 08:31:44 +03:00
Ümit Tunç ddbfc7e3a7 feat: replace static cover image with interactive 3D CD jewel case mockup and YouTube video lightbox 2026-06-01 08:31:21 +03:00
Ümit Tunç 59d9dfe40d feat: add detailed logging and directory verification for YouTube cover image storage failures 2026-06-01 08:15:49 +03:00
Ümit Tunç f6091f1bb8 feat: add company legal information section with clipboard copy functionality to intern dashboard 2026-06-01 08:09:32 +03:00
Ümit Tunç 57d092c0a3 feat: automate internship end date calculation based on business days in dashboard, Filament admin, and controller 2026-06-01 08:04:11 +03:00
Ümit Tunç 0649d38c48 feat: transition internship logic from manual dates to business-day-based duration calculation across admin and dashboard modules 2026-06-01 08:04:07 +03:00
Ümit Tunç aa48ce7890 feat: add internship date fields and business day calculation to career applications 2026-06-01 08:02:36 +03:00
Ümit Tunç e24b4bdf41 feat: add waiting_document status to career applications and localize status labels 2026-06-01 07:59:32 +03:00
Ümit Tunç 76081f0e85 feat: implement internship form upload functionality for interns with database migration and UI integration 2026-06-01 07:53:59 +03:00
Ümit Tunç e0941fe931 feat: update site typography by importing new Google Fonts and configuring global CSS font variables 2026-05-29 07:43:09 +03:00
Ümit Tunç 1ef535d132 feat: add SWIFT code field to bank accounts and update card UI for improved copy functionality and layout responsiveness. 2026-05-29 07:18:26 +03:00
Ümit Tunç b7ef2450bd feat: implement multi-language support for product and service menu headers 2026-05-27 07:13:16 +03:00
Ümit Tunç 38b06f9d1c refactor: optimize mega menu spacing and alignment using inline styles for consistent layout density 2026-05-27 07:07:34 +03:00
Ümit Tunç 5bc2fd0029 style: refine mega menu typography and spacing, and set explicit width for horizontal logo image 2026-05-27 07:01:47 +03:00
Ümit Tunç d39acfc582 style: update filter button pill shapes and replace iconography with logo in awards section 2026-05-27 06:56:28 +03:00
Ümit Tunç 5a1dfb51a2 refactor: remove trophy badge and update CTA styles to inline CSS for better compatibility 2026-05-27 06:54:22 +03:00
Ümit Tunç af2b57c6d1 refactor: adjust layout grid column widths and remove decorative image styling in odullerimiz template 2026-05-27 06:53:22 +03:00
Ümit Tunç dd0bee9355 feat: redesign awards template with new hero section and updated timeline filters 2026-05-27 06:51:35 +03:00
Ümit Tunç af147f5fea feat: implement dynamic hero cover image with fallback for odullerimiz template 2026-05-27 06:51:31 +03:00
Ümit Tunç 3c92a81cfe feat: implement full-stack awards module with Filament management, database support, and localization. 2026-05-27 06:32:31 +03:00
Ümit Tunç 05e8059b5d chore: remove legacy logo assets and add new award imagery 2026-05-27 06:25:51 +03:00
Ümit Tunç 5848cd5189 refactor: update product feature icon to text input and remove required validation from core product fields 2026-05-26 23:49:22 +03:00
Ümit Tunç c68e95631b feat: update finance page hero design, implement reusable hero buttons, and dynamic contact information 2026-05-26 23:42:46 +03:00
Ümit Tunç 8c4c58e094 refactor: update finance dashboard market cards to use flexbox layout for responsive wrapping 2026-05-26 23:33:35 +03:00
Ümit Tunç b7434e12c6 refactor: transition finance page to single-column layout with integrated logo header and grid-based rate cards 2026-05-26 23:32:10 +03:00
Ümit Tunç 121ba7d9c0 refactor: update finance dashboard grid layout and card styling to use Bootstrap utility classes 2026-05-26 23:31:23 +03:00
Ümit Tunç e054b04b06 ui: refactor live rates widget to use premium cards with SVG icons and updated typography 2026-05-26 23:29:27 +03:00
Ümit Tunç fe3d88c467 refactor: reorganize finance page layout by swapping rates and logo columns 2026-05-26 23:27:04 +03:00
Ümit Tunç 01a708b054 feat: add finance logo SVG and update finance product page 2026-05-26 23:27:00 +03:00
Ümit Tunç bd1ccb29c0 refactor: update finance page styles and layout and add finance logo asset 2026-05-26 23:25:36 +03:00
Ümit Tunç 8a08a0a89a feat: add Trunçgil Finance product with dedicated seeder and landing page views 2026-05-26 22:57:25 +03:00
Ümit Tunç 5d88241595 refactor: remove carousel section from home page template 2026-05-26 22:47:19 +03:00
Ümit Tunç 4ec2683317 refactor: redesign mega menu component to display products, services, and project links in a multi-column layout 2026-05-26 21:57:08 +03:00
Ümit Tunç 6fb2bfb265 style: add responsive top padding to akademi-hero section 2026-05-26 21:34:50 +03:00
Ümit Tunç 5f6c468a4b style: add teal button styles to truncgil-akademi template 2026-05-26 21:29:22 +03:00
Ümit Tunç 48ae773e85 refactor: rebrand Trunçgil Akademi theme from yellow to teal and add route alias for the academy page. 2026-05-26 21:29:14 +03:00
Ümit Tunç 4e98034536 feat: add dynamic scroll-based resizing for academy logo in header 2026-05-26 21:27:18 +03:00
Ümit Tunç f2b5a29b15 feat: replace remote akademi logo with local svg and clean up hero section styling 2026-05-26 21:27:12 +03:00
Ümit Tunç 3d2f2ab64e feat: add Trunçgil Akademi page template and automated seeder for course offerings 2026-05-26 21:21:58 +03:00
Ümit Tunç 7d7c018496 chore: increase PHP upload limits and update Livewire temporary file upload rules to 50MB 2026-05-26 21:07:48 +03:00
Ümit Tunç 92f14b2f4f feat: add revealable toggle to password field in career application form 2026-05-26 20:49:50 +03:00
Ümit Tunç bc59a32244 feat: add password generation action to career application form 2026-05-26 20:44:32 +03:00
Ümit Tunç 48f03cb83b refactor: remove redundant menu border and preload icon stylesheet for performance 2026-05-26 20:34:08 +03:00
Ümit Tunç 16f9d5a58a feat: add intern login link to navigation menus and update schema import path 2026-05-26 17:29:43 +03:00
Ümit Tunç a4bc64527c feat: implement intern authentication and secure document dashboard for internship applications 2026-05-26 17:21:30 +03:00
Ümit Tunç 910e912b05 feat: enhance Mermaid toolbar with improved hint display and interactive controls for zoom and fullscreen functionality 2026-05-25 07:59:49 +03:00
Ümit Tunç ba98e17d2d refactor: remove background styles from Mermaid canvas hint for improved clarity and responsiveness 2026-05-25 07:58:18 +03:00
Ümit Tunç abeed5ae88 feat: enhance Mermaid diagram interaction with improved activation, hint display, and click-to-zoom functionality 2026-05-25 07:57:42 +03:00
Ümit Tunç c28450b97d refactor: enhance mermaid fullscreen functionality with improved SVG handling and dynamic ID management 2026-05-25 07:47:29 +03:00
Ümit Tunç 40fbb750db fix: improve mermaid fullscreen rendering and sizing logic for pan-zoom initialization 2026-05-25 07:43:08 +03:00
Ümit Tunç e9ddc3d332 refactor: improve mermaid diagram container sizing and responsiveness using dynamic height calculation 2026-05-25 07:40:19 +03:00
Ümit Tunç 98355a5286 feat: add interactive zoom, pan, and fullscreen support for Mermaid diagrams using svg-pan-zoom 2026-05-25 07:37:27 +03:00
Ümit Tunç 5fc0462ab1 refactor: replace client-side clipboard copy logic with server-side JS injection and Filament notifications 2026-05-25 07:35:35 +03:00
Ümit Tunç 81b94e8635 feat: add proposal link regeneration and copy-to-clipboard actions to edit page 2026-05-25 07:32:25 +03:00
Ümit Tunç 92af30f61b refactor: remove Alpine.js dependency and simplify markdown preview rendering logic 2026-05-25 07:28:48 +03:00
Ümit Tunç 3cd5ee9e99 feat: implement tabbed interface with side-by-side markdown code editor and live preview for proposals 2026-05-25 07:26:26 +03:00
Ümit Tunç 8bf36c7d39 feat: update proposal layout to full-width and replace SVG logo with theme-aware images 2026-05-25 07:14:58 +03:00
Ümit Tunç a9fe4599cd feat: switch markdown content retrieval to hidden textarea to prevent escaping issues 2026-05-25 07:14:55 +03:00
Ümit Tunç f4500a490b refactor: update table action imports to use Filament Actions namespace 2026-05-25 07:07:42 +03:00
Ümit Tunç 547b6efc6c fix: remove integer formatting from views_count column in proposals table 2026-05-25 07:06:49 +03:00
Ümit Tunç c5a33f4d18 feat: implement proposal management system including database schema, Filament resources, and client-facing preview views 2026-05-25 07:05:57 +03:00
Ümit Tunç 15ba98e14d fix: resolve double-escaped newlines in code block copy functionality and update blog seeders 2026-05-25 06:58:33 +03:00
Ümit Tunç 0fe2775420 chore: update blog seeder data for content consistency 2026-05-25 06:55:33 +03:00
Ümit Tunç f69e7bae7b feat: add Schema.org structured data to blog index and post pages 2026-05-25 06:48:14 +03:00
Ümit Tunç c1dc4629c0 feat: integrate CodeMirror with custom macOS-style themes for code block syntax highlighting 2026-05-25 06:44:29 +03:00
Ümit Tunç 92f9147e80 feat: add new sample blog posts to BlogSeeder 2026-05-25 06:42:32 +03:00
Ümit Tunç 809f796f56 style: implement uniform layout clamping and flexbox sizing for blog post cards 2026-05-25 06:38:19 +03:00
Ümit Tunç 3b83f01952 feat: add initial seed data for blog posts to BlogSeeder 2026-05-25 06:36:11 +03:00
Ümit Tunç 654a7ed175 refactor: migrate blog post layout from classic view to responsive isotope grid with dynamic category styling and simulated engagement metrics 2026-05-25 06:19:49 +03:00
Ümit Tunç b4a81d703a feat: replace static category grid with swiper carousel and upgrade blog search UI 2026-05-25 06:18:44 +03:00
Ümit Tunç afa92d8f4a feat: implement premium blog search and filter UI with improved layout and dynamic post loading. 2026-05-25 06:01:35 +03:00
Ümit Tunç a037917f16 feat: add sample blog posts to BlogSeeder 2026-05-25 05:23:56 +03:00
Ümit Tunç 22c5e40cfc feat: implement dynamic blog category highlights with category-specific imagery and responsive grid layouts 2026-05-24 10:59:32 +03:00
Ümit Tunç 1b920e2b5b feat: add Truncgil Technology banner image to assets directory 2026-05-24 10:55:04 +03:00
Ümit Tunç 8c00956f29 feat: add avatar and role fields to User model and update BlogSeeder for Zeynep Demir profile 2026-05-24 10:49:49 +03:00
Ümit Tunç 3ed63cacb3 feat: implement BlogSeeder to populate database with initial blog categories and posts 2026-05-24 10:45:24 +03:00
Ümit Tunç b36ccf298b feat: implement blog homepage widgets including hero carousel, category highlights, and sidebar components 2026-05-24 10:37:32 +03:00
Ümit Tunç 2d6af94c88 feat: implement Dekupai product landing page with supporting assets and database seeder 2026-05-24 00:48:06 +03:00
Ümit Tunç dd013a471d feat: add GSAP scroll-triggered animation attributes to various sections and elements on the AI services page 2026-05-24 00:13:29 +03:00
Ümit Tunç 79ca5d49b6 feat: add responsive styling for hero AI illustration and clean up hero section layout and progress bar component 2026-05-24 00:11:33 +03:00
Ümit Tunç 5bf876d135 feat: update AI service page imagery and localization strings 2026-05-24 00:09:57 +03:00
Ümit Tunç 6665ed82b4 fix: update incorrect terminology in site translation strings 2026-05-24 00:01:38 +03:00
Ümit Tunç c668435b15 feat: update hero section content and CTA on AI services page 2026-05-24 00:00:02 +03:00
Ümit Tunç 6abec883c5 refactor: replace AI service hero content with networking template and update header layout 2026-05-23 23:59:26 +03:00
Ümit Tunç 1da2e3771a refactor: modernize hero and process sections with updated UI components and improved copy for the AI services template 2026-05-23 23:57:57 +03:00
Ümit Tunç a94fb17211 fix: escape JSON-LD schema keys by adding double at-symbols in yapay-zeka template 2026-05-23 23:52:14 +03:00
Ümit Tunç a61c80ee3d feat: add Artificial Intelligence service page and update homepage card link 2026-05-23 23:48:54 +03:00
Ümit Tunç 72c1234b5c refactor: remove contact button from header navigation in demo1 view 2026-05-23 23:41:27 +03:00
Ümit Tunç 2b15c17e60 refactor: remove floating doodle animations and standardize animation durations across components 2026-05-23 23:36:19 +03:00
Ümit Tunç 45c9fa24d0 refactor: dynamicize structured data fields and integrate entrance animations on web development page 2026-05-23 23:34:43 +03:00
Ümit Tunç c2767f031a style: update brand color palette from #f04e23 to #e31e24 across web development components 2026-05-23 18:02:02 +03:00
Ümit Tunç a484b19378 style: update web development service buttons with improved hover effects, shadows, and spacing 2026-05-23 18:00:20 +03:00
Ümit Tunç 7180d44f6d chore: update web development translation strings 2026-05-23 18:00:17 +03:00
Ümit Tunç 0c51fecef8 refactor: update brand color palette from purple to orange across web development template components 2026-05-23 17:53:59 +03:00
Ümit Tunç 945c3ca2ec fix: improve tech tabs layout by enforcing full-width flex alignment for navigation items 2026-05-23 17:52:35 +03:00
Ümit Tunç 945e250f17 style: add hover and active state styling for tech tabs grid headings and descriptions 2026-05-23 17:51:35 +03:00
Ümit Tunç fed91996fc feat: update web development service page with tech stack expansion and responsive grid layout 2026-05-23 17:50:52 +03:00
Ümit Tunç d1c2ccd985 feat: add extensive collection of technology and brand logos and update web development template 2026-05-23 17:41:06 +03:00
Ümit Tunç 3fb2106707 fix: update Material Symbols font link parameters for improved icon rendering across layouts 2026-05-23 13:58:44 +03:00
Ümit Tunç 7ae1dcfab8 feat: update web development template icons to Material Symbols and enhance styling for improved visual consistency 2026-05-23 13:50:10 +03:00
Ümit Tunç b2e6f89bcb feat: add SEO schema markup and page preloader to web development template 2026-05-23 11:13:20 +03:00
Ümit Tunç 53a265844f feat: add technology logos, implement WebDevelopmentPageSeeder, and update related service views and translations. 2026-05-23 11:06:36 +03:00
Ümit Tunç 607763ff52 feat: add new illustrations for app development section, including process, platforms, and call-to-action images, along with updated styles for improved responsiveness and visual consistency 2026-05-22 08:20:19 +03:00
Ümit Tunç 752876fed9 refactor: simplify header structure and enhance app development hero section styles for improved responsiveness and visual consistency 2026-05-22 08:09:31 +03:00
Ümit Tunç 08c1e28427 feat: add new platform icons for Android, iOS, Windows, macOS, Huawei, Meta Quest, and Ubuntu, along with updated styles for improved responsiveness in the app development section 2026-05-22 08:07:00 +03:00
Ümit Tunç 7e3700a2c2 feat: add new platform icons for Android, iOS, Windows, macOS, Huawei, Meta Quest, and Web, along with updated styling for improved responsiveness and accessibility in the app development section 2026-05-22 07:52:12 +03:00
Ümit Tunç 61cc5c8dda feat: update platform icons and badges with improved styling, responsive design, and enhanced accessibility for app development section 2026-05-22 07:36:33 +03:00
Ümit Tunç 11cb89c944 feat: implement page template hero functionality with dynamic image handling and localization support for enhanced visual presentation 2026-05-22 07:36:24 +03:00
Ümit Tunç ead0077fde feat: add platform icons and badges for Android, iOS, Windows, macOS, Huawei, Meta Quest, and Web to enhance app development section 2026-05-22 07:30:13 +03:00
Ümit Tunç f0ab100b07 feat: enhance app development section with responsive store badge layout and improved styling for better visual consistency 2026-05-22 07:23:40 +03:00
Ümit Tunç c195ff79d2 feat: update company history components with improved font loading, responsive styles, and enhanced visual consistency for better user experience 2026-05-22 07:20:39 +03:00
Ümit Tunç 474882ba7c feat: add new illustration for truncgil overview timeline and update styles for improved visual consistency across blog and component views 2026-05-22 05:34:04 +03:00
Ümit Tunç ddf9640222 feat: enhance security headers and optimize asset loading for improved performance and user experience 2026-05-21 22:00:58 +03:00
Ümit Tunç 6d9ffc808b feat: implement structured data support for blog and page components, enhancing SEO and user experience through JSON-LD integration 2026-05-21 21:54:12 +03:00
Ümit Tunç 8675325f50 feat: enhance header component with responsive navigation and tooltip support, improving user experience and accessibility 2026-05-21 20:54:28 +03:00
Ümit Tunç b678e7f833 feat: update company history seeder with Russian translations for various entries, enhancing multilingual support and user experience 2026-05-21 20:49:32 +03:00
Ümit Tunç ab08b6b840 feat: add new translation files for app development, corporate, home menu, and miscellaneous site content, enhancing multilingual support and user experience 2026-05-21 20:44:06 +03:00
Ümit Tunç d97a9ed69e feat: update company history seeder with new entries for Rhapsode Academic and Trunçgil Parabol Accounting, enhancing localization and expanding historical context 2026-05-21 20:33:55 +03:00
Ümit Tunç f75632d0e8 feat: enhance company history timeline with reveal animations and improved accessibility, optimizing user engagement and visual appeal 2026-05-21 20:28:27 +03:00
Ümit Tunç d24c9e4502 feat: improve company history timeline display by dynamically assigning positions based on index, enhancing visual consistency and user experience 2026-05-21 20:22:34 +03:00
Ümit Tunç 506a98163a feat: add quarter labels to company history localization files, improving clarity and user experience in timeline display 2026-05-21 20:21:54 +03:00
Ümit Tunç 2018860c23 feat: add quarter field to company history items with localization support, enhancing data organization and user experience 2026-05-21 20:16:32 +03:00
Ümit Tunç cc6ca1f660 feat: add new company history entry for IoT Dashboard with multilingual support, enhancing localization and user engagement 2026-05-21 20:15:09 +03:00
Ümit Tunç d6bfc44646 feat: add new company history entry for Stellar Construction with multilingual support, enhancing localization and user engagement 2026-05-21 20:13:30 +03:00
Ümit Tunç 42c3b3ac45 feat: expand company history module with additional translations, new illustration, and updated header layout for enhanced user experience 2026-05-21 20:05:11 +03:00
Ümit Tunç 69710a71b9 feat: enhance company history module with new font styles, improved localization functions, and optimized layout for better readability 2026-05-21 19:43:54 +03:00
Ümit Tunç 4150931ac8 feat: update company history module with new hero section, enhanced localization support, and improved layout for better user experience 2026-05-21 19:36:28 +03:00
Ümit Tunç c82dd5c6b6 feat: implement Company History module with CRUD functionality, localization support, and timeline display for enhanced user experience 2026-05-21 19:33:38 +03:00
Ümit Tunç a9d6f17e3c feat: enhance localization support by adding new translations for blog categories and home menu, improving multilingual capabilities across the platform 2026-05-21 10:25:08 +03:00
Ümit Tunç 502460be79 feat: enhance language selector component to prioritize specific languages for improved localization support and user experience 2026-05-21 10:17:48 +03:00
Ümit Tunç cda0bb49bc feat: update header component with new layout and responsive design for improved user experience and localization support 2026-05-21 10:16:43 +03:00
Ümit Tunç 55efbc6614 feat: add language flag emoji functions and enhance language selector component for improved localization support and user experience 2026-05-21 10:13:16 +03:00
Ümit Tunç 62540c4a98 feat: integrate custom header for app development page and enhance template structure for improved user experience 2026-05-21 09:55:23 +03:00
Ümit Tunç a79d00a8dd feat: update app development template to use new PNG images for devices, enhancing visual representation and consistency across the section 2026-05-21 09:52:33 +03:00
Ümit Tunç b51cf5876f feat: add new images for app development section and update template to use the new mobile app image for improved visual representation 2026-05-21 09:32:12 +03:00
Ümit Tunç 31a79ca9dd feat: update app development template with enhanced icon sizes for improved visual consistency and user experience 2026-05-21 09:21:53 +03:00
Ümit Tunç 0be000d474 feat: enhance app development template with new icon styles for improved visual representation and user experience 2026-05-21 09:11:24 +03:00
Ümit Tunç 6a4a0101a5 feat: add AppDevelopmentPageSeeder for populating app development page content and enhance app development template with new features, styling, and localization support for improved user experience 2026-05-21 09:06:01 +03:00
Ümit Tunç b487613b71 chore: remove outdated "What We Do" section template to streamline codebase and improve maintainability 2026-05-21 08:58:04 +03:00
Ümit Tunç a132da44f9 refactor: rename and enhance setting retrieval methods for improved clarity and functionality 2026-05-21 08:55:21 +03:00
Ümit Tunç da0ee2477b feat: implement language selector component with responsive design for improved user experience across devices 2026-05-21 08:53:44 +03:00
Ümit Tunç 1eab2c856b feat: improve site and partners templates with responsive design adjustments, ensuring better layout and user experience across devices 2026-05-21 08:41:30 +03:00
Ümit Tunç 1b00a3120e feat: update partners view with new collage layout, improved item styling, and responsive design for enhanced user experience 2026-05-21 00:52:11 +03:00
Ümit Tunç af36454b8f feat: add PartnersPageSeeder for populating partner data and enhance partners view with improved layout and styling for better user experience 2026-05-21 00:52:02 +03:00
Ümit Tunç fd30e5d1b7 feat: enhance partners section with new input fields, improved styling, and localization support for better user experience 2026-05-21 00:36:32 +03:00
Ümit Tunç dbb9853066 feat: enhance bank accounts template with new styling, localization support, and improved layout for better user experience 2026-05-21 00:36:19 +03:00
Ümit Tunç 0ca112b28d feat: add localization keys and update header template for "What We Do" section, enhancing multilingual support and UI structure 2026-05-21 00:29:23 +03:00
Ümit Tunç 34f790cc10 feat: add localization support for unknown production date in music productions module 2026-05-20 23:22:06 +03:00
Ümit Tunç 15896248c4 feat: enhance MusicProductionController with structured data support, improve SEO metadata handling, and add JSON-LD integration in views for better search engine visibility 2026-05-20 23:17:30 +03:00
Ümit Tunç 33c7b6a51e feat: implement search functionality in MusicProductionController, enhance view with search form and localization support for search results 2026-05-20 23:13:21 +03:00
Ümit Tunç 276e4b30ca refactor: update view structure in music productions index to improve semantic HTML by changing subtitle to a main heading and enhancing maintainability 2026-05-20 23:11:41 +03:00
Ümit Tunç 187f8c3812 refactor: optimize query usage in SyncSpotifyMusicProductions and SyncYoutubeReleases commands, enhance MusicProductionController by removing unused header logic, and update view structure for improved maintainability 2026-05-20 23:10:05 +03:00
Ümit Tunç ad4f04b005 refactor: streamline MusicProductionController by removing unused header logic, optimizing queries, and enhancing view data structure for improved maintainability 2026-05-20 23:03:49 +03:00
Ümit Tunç e18c8bbbcf feat: update YouTube API integration with enhanced release date parsing, improved topic channel resolution, and localization updates for music productions 2026-05-20 22:55:07 +03:00
Ümit Tunç ae3a697170 feat: enhance YouTube API integration by adding support for topic channel ID and improving release sync functionality with localization updates 2026-05-20 22:45:01 +03:00
Ümit Tunç e9f737ab1d feat: implement YouTube API integration for syncing releases with music productions, including new command and UI components 2026-05-20 22:39:20 +03:00
Ümit Tunç 751b9339f0 feat: integrate Spotify API for syncing music productions with new command and UI components 2026-05-20 22:31:41 +03:00
Ümit Tunç f35632ee0d feat: add Music Productions module with CRUD resources and frontend views 2026-05-20 22:15:04 +03:00
Ümit Tunç e232d25fc8 refactor: remove redundant inline styles and simplify Tailwind utility classes in language selector component 2026-05-20 08:40:23 +03:00
Ümit Tunç bc65bb10ef style: add mobile-specific styling to language selector dropdown for improved visibility 2026-05-20 06:28:02 +03:00
Ümit Tunç 3c5c4c5e2a style: update navbar responsive layout, language selector alignment, and body overflow handling 2026-05-20 06:25:01 +03:00
Ümit Tunç dbadfee9ed feat: add hero section branding image and update template to display it 2026-05-20 06:19:47 +03:00
Ümit Tunç 51ae8fdd9c feat: implement dynamic favicon management with site settings support across all layouts 2026-05-20 06:13:19 +03:00
Ümit Tunç 4168e1c8d4 feat: add localized hero title and extract underline styles, fix schema JSON syntax 2026-05-20 00:10:43 +03:00
Ümit Tunç 575fdad12c refactor: remove diagnostic and translation scripts and implement automatic cache clearing for settings in the model. 2026-05-20 00:06:01 +03:00
Ümit Tunç 19c9202fc1 feat: add automated scripts and JSON datasets for managing and translating site content 2026-05-20 00:03:07 +03:00
Ümit Tunç 29ddc24602 refactor: remove temporary translation script translate_site.php 2026-05-19 23:59:46 +03:00
Ümit Tunç fc0ebf1a28 feat: implement Arabic language support with RTL layout and automated translation scripts 2026-05-19 23:57:03 +03:00
Ümit Tunç bbe26d22af feat: add privacy policy template with bilingual support 2026-05-19 23:40:49 +03:00
Ümit Tunç 1950e8734c feat: localize landing page content and update icon asset paths 2026-05-11 10:04:58 +03:00
Ümit Tunç 3512858304 feat: internationalize internship application status messages and add Swedish/German language support 2026-05-11 09:23:07 +03:00
Ümit Tunç ebcbc9842c feat: localize career page and add mandatory NDA/contract document upload requirements to application form 2026-05-11 09:21:57 +03:00
Ümit Tunç 81790642c3 feat: add NDA/contract file uploads and technical skill checkboxes to career applications 2026-05-11 09:18:25 +03:00
Ümit Tunç 7cbf872b05 refactor: replace mermaid diagram with static ik-flowchat image on career page 2026-05-11 09:13:12 +03:00
Ümit Tunç 6a7e977330 refactor: overhaul career page with updated UI, NDA information, and issue-based workflow visualization 2026-05-11 09:13:08 +03:00
Ümit Tunç 5f26e76143 feat: add application type field to career applications and implement status filtering in admin panel 2026-05-11 08:28:48 +03:00
Ümit Tunç 932bb89454 refactor: migrate internship page layout to tailwind css styling 2026-05-11 08:05:38 +03:00
Ümit Tunç 2fd3dbb63f refactor: update internship page layout with new application requirements and localization strings 2026-05-11 08:01:41 +03:00
Ümit Tunç 9a606991e3 feat: update internship page with remote work details and application requirements 2026-05-11 08:00:21 +03:00
Ümit Tunç a81f4fdf9e feat: add internship application page and update career menu dropdown 2026-05-11 07:54:27 +03:00
Ümit Tunç 1547d07cb3 docs: update README header image width to percentage-based scaling 2026-05-11 06:37:59 +03:00
Ümit Tunç c5f2b7fc43 feat: Implement AJAX form submission for career application with loading states, SweetAlert2 notifications, and server-side JSON responses. 2026-03-11 09:12:20 +03:00
Ümit Tunç 7af164f3d7 feat: Update career form custom input styles with revised dimensions, box-shadow, responsive width, and placeholder properties. 2026-03-11 08:57:41 +03:00
Truncgil Technology 8f7c5e2102 Merge pull request #32 from truncgil/technical-debt-and-performance
Technical debt and performance
2026-03-11 08:52:15 +03:00
Muhammet Güler 45440f2814 feat: kariyer formu, mobil offcanvas menu ve blog modulu iyilestirmeleri 2026-03-10 19:55:25 +03:00
Muhammet Güler 4924cb0dc8 feat: kariyer formu, mobil offcanvas menu ve blog modulu iyilestirmeleri 2026-03-10 18:50:23 +03:00
Truncgil Technology f4f601a492 Merge pull request #31 from truncgil/feature/image-optimization-and-security
Feature/image optimization and security
2026-02-19 16:58:03 +03:00
Muhammet Güler 42357c4dbb Refactor payment page layout and perform security cleanup 2026-02-18 11:53:39 +03:00
Muhammet Güler 452d05f4a1 Enhance online payment button design: Oval, Red, Animated 2026-02-17 10:57:53 +03:00
Muhammet Güler 477993ec83 Optimize images to WebP and enable lazy loading. Add security headers (HSTS, CSP, etc) for A+ SSL score. 2026-02-17 10:30:36 +03:00
Truncgil Technology 2daa0f5ddd Merge pull request #28 from truncgil/fix/blog-assistant-and-seo
Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi
2026-02-16 17:14:05 +03:00
Muhammet Güler 7101f7878a Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi 2026-02-16 14:55:06 +03:00
Truncgil Technology a5847dcd58 Merge pull request #25 from truncgil/feature/online-payment
Fix payment form validation, styling and blog header visibility
2026-02-13 21:14:32 +03:00
Muhammet Güler 0fa8e4e435 Fix payment form validation, styling and blog header visibility 2026-02-13 12:45:17 +03:00
Truncgil Technology 8f85afd316 Merge pull request #24 from truncgil/feature/partners-and-bank-accounts
feat: add Partners and Bank Accounts pages via Pages module
2026-02-11 07:21:13 +03:00
Muhammet Güler bda19b4b0f feat: add Partners and Bank Accounts pages via Pages module 2026-02-10 19:02:23 +03:00
Truncgil Technology 7c65a4a592 Merge pull request #23 from truncgil/fix-and-ourlogos
Fix and ourlogos
2026-02-09 18:44:16 +03:00
Muhammet Güler b289cfd829 feat: re-enable image editor and aspect ratios for blog and page settings 2026-02-09 15:18:59 +03:00
Muhammet Güler 177ebff119 feat: Add Logos page, refactor Testimonials to RelationManager, and improve designs 2026-02-08 04:54:32 +03:00
Truncgil Technology 33c1ffe779 Merge pull request #21 from truncgil/musteri-gorusleri
Müşteri Görüşleri Sayfası & Genel Düzenlemeler
2026-02-04 19:25:29 +03:00
Muhammet Güler e4ca3b228b Müşteri Görüşleri Sayfası & Genel Düzenlemeler
* Yeni Sayfa: Kurumsal menüsü altına modern tasarımlı 'Müşteri Görüşleri' sayfası eklendi.\n* Düzeltme: Kitaplar sayfasındaki renk döngüsü hatası giderildi ve eksik Google Play linki eklendi.\n* Temizlik: Proje dizinindeki gereksiz bakım dosyaları silindi.
2026-02-04 17:32:38 +03:00
Truncgil Technology 2f9fb14bac Merge pull request #20 from truncgil/muhammetguler
Page errors on the "Our Books" page have been corrected.
2026-02-04 07:41:53 +03:00
Muhammet Güler 43bcca4a5e Page errors on the Our Books page have been corrected. 2026-02-04 00:30:43 +03:00
Muhammet Güler dbd267ca17 pr deneme 2026-02-03 14:20:36 +03:00
Muhammet Güler 74779ef2c8 Update PageSeeder 2026-02-03 13:15:43 +03:00
Muhammet Güler 4a73f7a3a0 Merge pull request #17 from truncgil/muhammetg
Add Books page and minor improvements
2026-02-03 03:36:48 +03:00
Muhammet Güler 03ff01f24d Add Books page and minor improvements 2026-02-03 02:31:54 +03:00
Muhammet Güler 90565412a4 Kitaplarımız sayfası eklendi 2026-02-02 12:51:46 +03:00
Ümit Tunç dc63563e7f refactor: remove highlight spans and simplify description paragraphs in the hero section. 2026-01-30 21:46:01 +03:00
Ümit Tunç 1400ec1ca7 Enhance Search Functionality in Site Translations DataGrid: Introduced a new helper function to extract search values from filters, improving the search capability within the DataGrid. Updated the search logic to utilize this helper, ensuring more effective filtering based on user input. This enhancement aims to provide a more intuitive and efficient user experience when managing site translations. 2026-01-10 23:20:23 +03:00
Ümit Tunç 7946876434 Enhance Site Translations DataGrid Functionality: Updated the DataGrid view to include the wire:ignore directive for improved Livewire compatibility. Adjusted the theme application logic to dynamically respond to changes in the document's class, ensuring a seamless user experience across different themes. Enabled remote operations for the DataGrid, enhancing data handling capabilities. 2026-01-10 23:20:17 +03:00
Ümit Tunç ca942c5140 Refactor Site Translations List Page and Enhance DataGrid Functionality: Updated the ListSiteTranslations page to improve session handling for view toggling between traditional table and DataGrid formats. Enhanced the getHeaderActions method for better readability and session management. Additionally, modified the render method to ensure the latest session value is utilized for rendering the appropriate view. Updated the DataGrid view to dynamically adjust the theme based on the current body class, improving user experience across different themes. 2026-01-10 22:59:58 +03:00
Ümit Tunç e80c94146d Implement Site Translations API and DataGrid View: Introduced a new API controller for managing site translations, including methods for listing, storing, updating, and deleting translations. Added a DataGrid view for the site translations page, allowing for enhanced user interaction and management of translations. Implemented session-based view toggling between traditional table and DataGrid formats, improving usability. Localization support has been integrated for all text elements, ensuring a seamless experience for users across different languages. 2026-01-10 21:54:56 +03:00
Ümit Tunç 2fdf187658 Implement GSAP Features in 'neler-yapariz-gsap' Template: Introduced a comprehensive set of GSAP animations and sections in the 'neler-yapariz-gsap' template, enhancing the visual dynamics of the page. This update includes new JavaScript and CSS files for various animations, such as flip, inertia, marquee, and scrolltrigger, along with a structured layout for improved user interaction. Additionally, localization support has been integrated for all text elements, ensuring a seamless experience for users across different languages. 2026-01-10 17:47:51 +03:00
Ümit Tunç 3ad69677d8 Add New GSAP Images and Update 'neler-yapariz' Template: Introduced new images for GSAP features, enhancing visual content in the 'neler-yapariz' template. Updated the template to include these images in the flip section, improving the overall aesthetic and user engagement. Additionally, made localization adjustments to ensure all text elements are translatable, further enhancing the user experience across different languages. 2026-01-10 17:12:35 +03:00
Ümit Tunç 6caa15b501 Update 'neler-yapariz' Navigation and CSS Styles: Adjusted the sub-navigation background positioning in the 'neler-yapariz' template to improve visual alignment. Updated the CSS to modify the transform property for the sub-navigation background, enhancing the overall layout and user experience. This change aims to create a more polished and cohesive design across the template. 2026-01-09 21:48:55 +03:00
Ümit Tunç 171132263b Update Toggle Styles in Additional CSS: Changed the background and border colors of the toggle ball in the 'neler-yapariz' template's additional CSS file to #ff812d for improved visual consistency with the overall design. This update enhances the user interface and aligns with recent styling changes. 2026-01-09 21:48:31 +03:00
Ümit Tunç 9e79ec28d9 Update 'neler-yapariz' Navigation Template and CSS Styles: Revamped the navigation structure in the 'neler-yapariz' template by replacing the SVG logo with an image and enhancing the sub-navigation layout for better user interaction. Additionally, updated CSS variables for improved color consistency and modified toggle slider styles to align with the new design. This update aims to enhance visual appeal and user experience across the site. 2026-01-09 21:40:01 +03:00
Ümit Tunç 3b852512f0 Refactor 'neler-yapariz' Template and Introduce New Sections: Updated the 'neler-yapariz' template to enhance its structure by adding new sections for footer, hero, inertia, marquee, and scrolltrigger. Introduced external CSS and JS files for improved styling and functionality. This update aims to enhance user experience through better organization and dynamic content integration. 2026-01-09 21:16:13 +03:00
Ümit Tunç b00ca5ea78 Add GSAP Layout and Animated Logo Component: Introduced a new layout file for GSAP integration, enhancing the site's animation capabilities. Added an animated logo component to improve visual engagement. Updated the 'neler-yapariz' template to utilize the new GSAP layout, ensuring a cohesive design and better user experience. 2026-01-09 21:03:54 +03:00
Ümit Tunç c73d448f47 Implement Dynamic Header and Footer Templates in Blog Controller: Enhanced the BlogController to prioritize page-specific header and footer templates based on the 'blog' page settings. If no specific templates are set, it falls back to the default active templates. Updated the blog index view to include a header template variable for better customization. Additionally, added redirects for legacy index URLs to improve user navigation. 2026-01-06 22:03:05 +03:00
Ümit Tunç 374d4a8d7d Enhance Translation Handling in HasTranslations Trait: Improved the translate method to ensure non-empty values are returned from the translations table and original field values. Added JSON handling for original values to support array-based translations. This update enhances localization support and ensures more reliable translation retrieval across different languages. 2026-01-06 21:42:33 +03:00
Ümit Tunç 3a2661cf95 Refactor Landing Page Section and Product Form: Removed required validation from several fields in the LandingPageSection schema to enhance flexibility. Updated the ProductForm to enforce required validation on the hero image field, ensuring essential data is captured for product management. Improved localization support by ensuring all labels utilize the translation function. 2026-01-06 21:36:13 +03:00
Ümit Tunç 36471b5965 Add External URL Field to Product Schema and Localization: Introduced an 'external_url' field in the ProductForm for enhanced product management. Updated the Product model to include this new field and created a migration to add it to the products table. Enhanced localization support by adding relevant translations in both English and Turkish language files. Updated the menu-item Blade component to utilize the external URL, allowing links to open in a new tab if provided, improving user experience. 2026-01-06 21:18:25 +03:00
Ümit Tunç 194e575619 Update Header and Add New Home Template: Enhanced the header layout by including a language selector for improved localization support. Introduced a new 'neler-yapariz' template that integrates various sections of the home page, ensuring a cohesive structure and better user experience. 2026-01-02 23:32:03 +03:00
Ümit Tunç a1d02de86b Update Landing Page Layout and Improve Responsiveness: Adjusted the layout of the landing page by modifying the width of various sections for better responsiveness. Enhanced the styling of the featured image section and added a new divider for visual appeal. Updated the FAQ section header to utilize localization for improved language support, ensuring a consistent user experience across different languages. 2026-01-02 22:58:29 +03:00
Ümit Tunç 1fc23e7bda Update Header Template in Landing Page Blade: Changed the header template from 'Demo29 Header' to 'Demo27 Header' in the landing page view, ensuring consistency with the updated design and layout standards. 2026-01-02 20:58:30 +03:00
Ümit Tunç 97092dd793 Add Landing Page Section and Update Product Schema: Introduced a new LandingPageSection class to manage the layout and fields for the product landing page. Updated the ProductForm to include this new section, enhancing the product management interface. Added migration for landing_page_data in the products table and implemented localization for new fields in English and Turkish language files. Created a new landing Blade view to render the landing page dynamically, improving user experience and modularity. 2026-01-02 20:58:18 +03:00
Ümit Tunç 89eb9f5c4e Refactor Featured Products Section in Menu Item Component: Updated the layout and styling of the featured products section in the menu-item Blade component for improved visual appeal and user experience. Enhanced the structure for better responsiveness and consistency in design. 2026-01-01 20:00:34 +03:00
Ümit Tunç 0ca75829bb frontend style fix 2026-01-01 16:25:27 +03:00
Ümit Tunç e8cdba4fd3 Refactor Product and Category Tables for Enhanced Localization: Updated the ProductCategoriesTable and ProductsTable to utilize the getStateUsing method for retrieving translated titles, improving localization support. Enhanced the search functionality to filter by translations, ensuring consistent display of titles across different languages. Additionally, modified the menu-item Blade component to include featured products, improving the layout and user experience. 2026-01-01 11:13:45 +03:00
Ümit Tunç 12441517f6 Add Custom Header and Footer Blade Selection: Implemented options for selecting custom Blade files for header and footer templates in the Page settings. Enhanced the PageController to render these templates dynamically, improving modularity and user experience. Updated relevant views to support this new functionality, ensuring better localization and flexibility in template management. 2026-01-01 10:54:38 +03:00
Ümit Tunç 10f2e9c9bb Refactor Product Form Localization: Updated the category options in the ProductForm to utilize the translation function for titles, enhancing localization support and ensuring consistent display of category names across different languages. 2026-01-01 09:58:31 +03:00
Ümit Tunç 5aa2a3d6e5 Enhance Settings and PageController Logic: Updated the SettingForm to include dynamic selection for header and footer templates based on active settings, improving modularity and user experience. Refactored PageController to retrieve active settings for rendering templates, ensuring fallback logic is streamlined and enhancing overall functionality. Updated navbar component to dynamically display social media links from settings, contributing to better localization and user engagement. 2026-01-01 09:55:16 +03:00
Ümit Tunç 3e4fc26e4b Remove Unused What We Do Section from Home Template: Commented out the inclusion of the 'what-we-do' component in the home template, streamlining the layout and improving overall organization. This change contributes to a cleaner design and better modularity. 2026-01-01 00:30:14 +03:00
Ümit Tunç 68b7907981 Refactor Header and Why Choose Us Templates: Updated the header template to dynamically retrieve social media links from settings, enhancing functionality and localization support. Modified button styles in the Why Choose Us section for a more consistent rounded appearance, improving overall design coherence. 2026-01-01 00:22:50 +03:00
Ümit Tunç d75434674f Add Dynamic Menu Item Component: Introduced a new Blade component for front-end menu items, enhancing the navigation structure with support for mega menus and nested children. Updated the header template to utilize this component, improving modularity and localization for menu titles. 2026-01-01 00:12:46 +03:00
Ümit Tunç cb0bbf87c7 Update Header and Home Templates: Modified the header template to dynamically retrieve contact information and social media links from settings, enhancing functionality and localization support. Updated the hero section background image for improved visual appeal. Simplified the services cards layout by removing unnecessary padding, contributing to a cleaner design. 2026-01-01 00:04:00 +03:00
Ümit Tunç 77a2713aec Implement Home Page Structure: Refactored the home template to include multiple new components for improved modularity and organization. Added sections for hero, services, solutions, testimonials, and contact information, enhancing the overall layout and user experience. Ensured all new components utilize localization for text content, maintaining consistency across languages. 2025-12-31 23:47:02 +03:00
Ümit Tunç 1fa194260e Refactor Localization in Templates: Updated various templates to utilize the translation function for titles and content, enhancing localization support. Commented out unused fallback logic in the PageController for header templates, streamlining the rendering process. Added new header and navbar components while removing the deprecated navbar file, improving the overall structure and maintainability of the layout. 2025-12-31 23:35:42 +03:00
Ümit Tunç 009d1cbb76 Update Footer Template: Refactored social media links to dynamically retrieve URLs from settings, enhancing the footer's functionality. Improved localization support by ensuring all links are properly formatted and accessible, contributing to a better user experience. 2025-12-31 22:27:10 +03:00
Ümit Tunç 2a1cdafbea Enhance EditSetting Page: Added a method to mutate form data before filling, allowing dynamic retrieval of virtual field values based on the selected type. Updated footer template to utilize localization for contact information and section titles, improving multilingual support and user experience. 2025-12-31 22:16:42 +03:00
Ümit Tunç b345c6f71c Refactor Footer Template Logic: Commented out the fallback logic for footer templates in the PageController to streamline rendering. Updated the site layout to check for non-empty rendered footer content, enhancing the footer display logic. Cleaned up the home template by removing unnecessary characters and ensuring proper formatting. 2025-12-31 22:08:10 +03:00
Ümit Tunç a2827c5d7a a 2025-12-31 22:00:45 +03:00
Ümit Tunç 194beaee1c iniit 2025-12-31 21:37:21 +03:00
Ümit Tunç cf855fdd70 Update Home Template: Translated key sections to Turkish, enhancing localization support. Added new content blocks for services offered, including Web Design and Mobile Design, with corresponding descriptions and links. Improved layout structure for better responsiveness and user experience. 2025-12-31 01:21:30 +03:00
Ümit Tunç 6b561896c6 Enhance SiteTranslationImporter: Added 'Key' label to the import column and simplified validation rules. Improved record resolution logic to handle empty keys and ensured translations are updated correctly during import. Updated CSS paths for assets in the home template for better resource management. 2025-12-31 01:01:41 +03:00
Ümit Tunç 922add1b6f Add Import and Export Functionality for Site Translations: Implemented SiteTranslationImporter and SiteTranslationExporter classes to facilitate the import and export of site translations. Enhanced the SiteTranslationsTable with import and export actions, allowing for efficient management of translation data. Updated the migration files to create necessary tables for handling imports and exports, improving the overall localization workflow in the admin panel. 2025-12-31 00:23:16 +03:00
Ümit Tunç 2c9dcac76c Refactor SettingForm Schema: Removed the image validation from the file input field in the SettingForm schema, ensuring that only specified file types are accepted for upload. This change enhances the flexibility of file handling in the settings management. 2025-12-31 00:01:39 +03:00
Ümit Tunç 064ec327ba Add Site Translation Management: Created SiteTranslation resource with pages for listing, creating, and editing translations. Implemented localization support for translation keys and values, enhancing the admin panel's usability for managing site translations. Introduced a new migration for the site_translations table and updated helper functions for translation retrieval. 2025-12-30 23:41:32 +03:00
Ümit Tunç f381caaa39 Implement Localization for Product and ProductCategory Resources: Refactored resource classes and forms to utilize Laravel's localization features, enhancing user experience by providing translated labels and sections. Added English and Turkish language support for product and category management, ensuring consistency across the admin panel. 2025-12-30 22:49:55 +03:00
Ümit Tunç 54944317b3 Add Language Support to Admin Panel: Implemented dynamic language menu items in the admin panel by retrieving active languages from the database. Enhanced user experience by allowing language selection directly from the user menu, improving localization capabilities. 2025-12-30 22:28:20 +03:00
Ümit Tunç f4cf16f93f Refactor Product Category Form and Product Form: Updated the options method to use \Illuminate\Support\Arr::first for retrieving category titles, ensuring better handling of localization. Added a fallback for category titles to display a default message when no title is available, improving user experience in form submissions. 2025-12-30 22:16:51 +03:00
Ümit Tunç a85e6eebe0 Implement Product and ProductCategory Resources: Created new Filament resources for managing products and product categories, including pages for listing, creating, and editing. Added schemas for forms and tables, integrated translation support, and established relationships between products and categories. Enhanced the navigation structure to include product and service links in the frontend menu, improving overall site organization and user experience. 2025-12-30 22:16:45 +03:00
Ümit Tunç a5a3248c69 Add Frontend Theme Implementation Rules: Introduced a new markdown file outlining the rules for converting static HTML templates in the public/html directory to Laravel Blade structure. This includes guidelines on source and target structure, component organization, code conversion rules, CMS integration, and automatic menu configuration, enhancing the development process for frontend themes. 2025-12-30 21:18:02 +03:00
Ümit Tunç 21ebbfc749 Update Dependencies and Add BlogCategoryPolicy: Upgraded the version of the 'anourvalar/eloquent-serialize' package to 1.3.5 and added a new BlogCategoryPolicy class to manage authorization for blog category actions. This enhances the application's security and maintainability by implementing specific access controls. 2025-12-30 20:53:31 +03:00
2020 changed files with 405605 additions and 9211 deletions
+92
View File
@@ -0,0 +1,92 @@
---
description: Frontend blok ve section mimarisi kuralları. Yeni blok eklerken veya sayfa yapısı kurarken bu kuralları uygula.
globs: resources/views/components/blocks/**/*.blade.php, app/Filament/**/*.php, resources/views/templates/**/*.blade.php
---
# Frontend Architecture Rules
Bu kurallar, projenin Blok (Block) ve Bölüm (Section) tabanlı mimarisini tanımlar. Sayfa oluştururken bu standartlara uyulmalıdır.
## 1. Blok (Block) Yapısı
Sistem, `resources/views/components/blocks/` altında bulunan hazır Blade bileşenleri üzerine kuruludur.
### Mevcut Bloklar ve Kullanımları
- **Hero:** Ana başlık, büyük görsel ve CTA butonları.
- **Features:** İkonlu özellik kutuları grid yapısı.
- **Stats:** Sayısal istatistikler ve sayaçlar.
- **Testimonials:** Müşteri yorumları ve referanslar.
- **Pricing:** Fiyatlandırma tabloları.
- **CTA:** Aksiyon çağrısı bantları.
- **Gallery:** Proje veya görsel galerisi.
- **Team:** Ekip üyeleri listesi.
- **Contact:** İletişim bilgileri ve formu.
### Blok Geliştirme Kuralları
1. **Component Yapısı:** Her blok `resources/views/components/blocks/{type}.blade.php` konumunda olmalıdır.
2. **Veri Alımı:** Bloklar veriyi sadece `$data` prop'u üzerinden almalıdır:
```blade
@props(['data' => []])
{{-- Erişim: $data['title'] --}}
```
3. **Null Check:** Tüm veri alanları için `isset` veya `??` kontrolü yapılmalıdır.
```blade
{{ $data['title'] ?? '' }}
@if(isset($data['image'])) ... @endif
```
## 2. Section Builder ve Veri Yapısı
Admin panelinde "Section Builder" kullanılarak veriler JSON formatında tutulur.
### Veri Formatı (JSON)
Veriler `sections` sütununda veya `parsed_sections` attribute'unda şu formatta işlenir:
```json
[
{
"type": "hero",
"data": {
"title": "Başlık",
"subtitle": "Alt Başlık",
"background_image": "assets/img/bg.jpg"
}
}
]
```
### Key İsimlendirme Kuralları (ÖNEMLİ)
Veri anahtarları (keys) her zaman **İngilizce** ve **snake_case** olmalıdır:
- ✅ `title`, `sub_title`, `button_text`, `image_url`
- ❌ `baslik`, `altBaslik`, `butonYazisi`, `resim`
### Önerilen Value Tipleri
Admin panelinde alan tanımlarken doğru tipleri seçin:
- **Kısa Metinler:** `Text` (title, badge)
- **Uzun Metinler:** `Textarea` (description)
- **Formatlı İçerik:** `Rich Text` veya `HTML` (content)
- **Görseller:** `Image` (background_image, icon)
- **Linkler:** `URL` (button_url)
## 3. Blade Entegrasyonu
Sayfa şablonlarında (örneğin `home.blade.php`) blokları render ederken dinamik yapı kullanılmalıdır:
```blade
@foreach($page->parsed_sections as $section)
@if(view()->exists("components.blocks.{$section['type']}"))
<x-dynamic-component
:component="'blocks.'.$section['type']"
:data="$section['data']"
/>
@endif
@endforeach
```
## 4. Stil Özelleştirme
Bloklar, dışarıdan gelen stil parametrelerini desteklemelidir:
- `bg_class`: Arka plan rengi (örn: `!bg-[#f0f0f8]`)
- `column_class`: Grid yapısı (örn: `md:w-4/12`)
- `padding_class`: Boşluklar (örn: `py-14`)
Bu sınıflar Blade içinde ana kapsayıcıya (wrapper) eklenmelidir.
+138
View File
@@ -0,0 +1,138 @@
---
description: Public HTML şablonlarını Laravel Blade yapısına dönüştürme kuralları
globs: resources/views/**/*.blade.php, public/html/**/*.html
---
# Frontend Theme Implementation Rules
Bu kurallar, `public/html` dizinindeki statik HTML şablonlarının Laravel Blade yapısına dönüştürülmesi sırasında geçerlidir.
## 1. Kaynak ve Hedef Yapısı
- **Kaynak (Source):** `public/html/` dizinindeki `.html` dosyaları referans alınır.
- **Hedef (Target):** `resources/views/front/` dizini altında Blade dosyaları oluşturulur.
- **Assets (Varlıklar):** Statik dosyalar `public/html/assets` altındadır. Blade içinde erişim `asset('html/assets/...')` şeklinde olmalıdır.
## 2. Bileşen (Component) Yapısı
Yeni UI bileşenleri oluştururken aşağıdaki isimlendirme ve klasör yapısını kullan:
- **Layout:** `resources/views/layouts/front.blade.php` (Master layout)
- **Header:** `resources/views/components/front/header.blade.php` -> `<x-front.header />`
- **Footer:** `resources/views/components/front/footer.blade.php` -> `<x-front.footer />`
- **Page Sections:** `resources/views/components/front/sections/` altında:
- Hero/Slider -> `<x-front.sections.hero />`
- Features -> `<x-front.sections.features />`
- About -> `<x-front.sections.about />`
## 3. Kod Dönüştürme Kuralları
### A. CSS ve Sınıflar
- **ÖNEMLİ:** Şablondaki `!` önekli Tailwind sınıflarını (örn: `!bg-[#f0f0f8]`, `!absolute`) KESİNLİKLE KORU. Bunlar temanın çalışması için kritiktir.
- HTML'deki `class="..."` yapılarını birebir kopyala, Tailwind sınıflarını sadeleştirme veya değiştirme.
### B. Asset Bağlantıları
HTML içindeki:
```html
<link rel="stylesheet" href="assets/css/plugins.css">
<img src="assets/img/logo.png">
```
Blade içinde şu şekilde olmalı:
```blade
<link rel="stylesheet" href="{{ asset('html/assets/css/plugins.css') }}">
<img src="{{ asset('html/assets/img/logo.png') }}">
```
### C. Dinamik İçerik
- Metinleri `{{ __('theme.key') }}` şeklinde lokalizasyon fonksiyonu ile sar.
- Tekrarlayan elemanları (örn: slider itemları, hizmet kutuları) `@foreach` döngüsü içine al.
## 4. Layout Şablonu (Örnek)
`front.blade.php` layout dosyası şu yapıda olmalıdır:
```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<!-- CSS Assets from public/html/index.html head -->
@stack('styles')
</head>
<body class="!bg-[#f0f0f8]">
<x-front.header />
<main class="grow shrink-0">
@yield('content')
</main>
<x-front.footer />
<!-- JS Assets from public/html/index.html bottom -->
@stack('scripts')
</body>
</html>
```
## 5. CMS Entegrasyonu (Dinamik Şablon Seçimi)
Kullanıcı, bir prompt ile belirli bir section, header veya footer şablonunun kullanılmasını istediğinde (örn: "Header V1 ve About Section 3 kullan"), aşağıdaki stratejiyi izle:
1. **Veritabanı Araması:** İlgili şablonun veritabanında (genellikle `sections` veya `templates` tablosunda) var olduğunu varsay.
2. **Seeder/Migration Oluşturma:** Kullanıcının istediği yapıyı oluşturmak için bir Laravel Seeder veya Migration kodu üret. Bu kod:
- İlgili şablonu (title veya slug ile) bulmalı.
- Sayfa veya layout ile ilişkilendirmeli.
**Örnek Senaryo:** "Ana sayfada 'Hero Slider V2' ve 'Services Grid' kullan."
**Üretilecek Kod (Örnek):**
```php
// DatabaseSeeder.php veya özel bir seeder içinde
$homePage = Page::firstOrCreate(['slug' => 'home'], ['title' => 'Ana Sayfa']);
// Şablonları bul
$heroSection = Section::where('title', 'like', '%Hero Slider V2%')->first();
$servicesSection = Section::where('title', 'like', '%Services Grid%')->first();
// İlişkilendir (Proje yapısına göre attach veya update)
if ($heroSection) {
// Örnek ilişkilendirme mantığı
$homePage->sections()->attach($heroSection->id, ['order' => 1]);
}
```
## 6. Otomatik Menü ve Sayfa Yapılandırması
Kullanıcı "Şu yapıda bir menü tasarla" dediğinde (örn: "Ana Sayfa, Kurumsal > Hakkımızda, İletişim"):
1. **Page Modeli:** İstenen sayfaların veritabanında var olup olmadığını kontrol eden ve yoksa oluşturan bir kod bloğu hazırla.
2. **Menu Modeli:** Sayfaları hiyerarşik olarak menü yapısına ekleyen bir kod bloğu hazırla.
3. **Çıktı:** Bu işlemleri gerçekleştirecek bir `php artisan tinker` komutu veya `MenuSeeder` sınıfı sun.
**Örnek Prompt:** "Menü: Ana Sayfa, Kurumsal (Hakkımızda, Vizyon), İletişim"
**Beklenen Aksiyon:**
```php
// 1. Sayfaları Oluştur
$pages = [
'Ana Sayfa' => '/',
'Kurumsal' => '#',
'Hakkımızda' => '/kurumsal/hakkimizda',
'Vizyon' => '/kurumsal/vizyon',
'İletişim' => '/iletisim'
];
foreach ($pages as $title => $url) {
Page::firstOrCreate(['title' => $title], ['slug' => Str::slug($title)]);
}
// 2. Menü Yapısını Kur
$mainMenu = Menu::firstOrCreate(['name' => 'Main Menu']);
// ... Menü elemanlarını ekleyen kod ...
```
## 7. Analiz Stratejisi
Bir sayfayı dönüştürürken:
1. Önce `public/html` altındaki ilgili `.html` dosyasını oku.
2. `header` ve `footer` kısımlarını ayır.
3. `main` içeriğindeki her bir `section` veya mantıksal bloğu ayrı bir Blade component olarak çıkar.
+85
View File
@@ -0,0 +1,85 @@
---
description: Google yapılandırılmış veri (JSON-LD) standartları
globs: app/Support/*StructuredData*.php, app/Http/Controllers/**/*.php, resources/views/**/*.blade.php
---
# Yapılandırılmış Veri (Structured Data) Kuralları
Google [Yapılandırılmış Veri Genel Yönergeleri](https://developers.google.com/search/docs/appearance/structured-data/sd-policies?hl=tr) ve [Giriş](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data?hl=tr) dokümanlarına uy.
## Temel İlkeler
1. **JSON-LD kullan** — Google'ın önerdiği format; `<head>` içinde `<script type="application/ld+json">` ile sunulur.
2. **Sayfa içeriğiyle eşleş** — İşaretlenen veri kullanıcının gördüğü içerikle birebir uyumlu olmalı; görünmeyen veya yanlış bilgi ekleme.
3. **Eksiksiz ve doğru özellikler** — Zorunlu alanları doldur; eksik/hatalı önerilen alanları doldurmaya çalışmak yerine az ama doğru veri tercih et.
4. **schema.org sözlüğü** — `@context: https://schema.org` kullan; Google Arama özellikleri için [Search Central özellik rehberlerini](https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=tr) referans al.
## Proje Mimarisi
```
app/Support/
├── StructuredData.php # Ortak yardımcılar (WebSite, Organization, WebPage, BreadcrumbList)
├── PageStructuredData.php # CMS sayfaları
├── BlogStructuredData.php # Blog listesi ve detay
└── MusicProductionStructuredData.php
```
### Controller → View akışı
```php
// Controller
'structuredData' => BlogStructuredData::forShow($post, url()->current()),
// Layout (layouts/site.blade.php) otomatik render eder:
@if(!empty($structuredData))
<x-seo.json-ld :data="$structuredData" />
@endif
```
**Asla** Blade içinde ham JSON string interpolasyonu kullanma — `json_encode` ile `<x-seo.json-ld>` bileşenini kullan.
## Sayfa Türü → Schema Eşlemesi
| Sayfa türü | Schema türleri | Sınıf |
|---|---|---|
| Ana sayfa / CMS sayfaları | WebSite, Organization, WebPage, BreadcrumbList | `PageStructuredData` |
| Blog listesi | CollectionPage, ItemList, BlogPosting (özet) | `BlogStructuredData::forIndex` |
| Blog detay | BlogPosting, WebPage, BreadcrumbList | `BlogStructuredData::forShow` |
| Müzik prodüksiyonları | CollectionPage / MusicAlbum, ItemList | `MusicProductionStructuredData` |
| Ürün/hizmet sayfaları | WebPage, Product veya Service | Henüz eklenmedi |
| Kariyer / staj | WebPage | Henüz eklenmedi |
| İletişim | WebPage, Organization (contactPoint) | Henüz eklenmedi |
## @graph Deseni
Birden fazla entity için `@graph` kullan; `@id` ile cross-reference yap:
```php
return self::wrap([
self::websiteNode(),
self::organizationNode(),
self::webPageNode($pageUrl, $name, $description),
self::breadcrumbNode($pageUrl, $items),
// sayfa-özel entity (BlogPosting, MusicAlbum, vb.)
]);
```
## Yeni Sayfa Eklerken
1. `app/Support/` altında `*StructuredData.php` sınıfı oluştur veya mevcut sınıfı genişlet.
2. `StructuredData` base sınıfındaki ortak node'ları kullan (DRY).
3. Controller'da `structuredData` key'ini view'a geçir.
4. Görünür içerikle eşleştiğini doğrula (başlık, açıklama, görsel, tarih).
5. [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results) ile doğrula.
## Yasaklar
- ❌ Boş veya içeriksiz sayfalara yalnızca schema eklemek
- ❌ Blade'de `"headline": "{{ $title }}"` gibi kaçışsız JSON
- ❌ `@push('scripts')` ile body sonuna JSON-LD koymak (head'de olmalı)
- ❌ data-vocabulary.org işaretlemesi
- ❌ Kullanıcıya görünmeyen rating/review verisi uydurmak
## Doğrulama
- Geliştirme: [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results)
- Prod: Search Console → Zengin sonuçlar raporları
+21
View File
@@ -63,3 +63,24 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# Spotify Web API (Müzik Prodüksiyonları senkronizasyonu)
# NOT: Developer Dashboard'da uygulamayı oluşturan Spotify hesabında Premium abonelik gerekir.
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_ARTIST_ID=
SPOTIFY_MARKET=TR
# YouTube Data API v3 (Müzik Prodüksiyonları / Art Track senkronizasyonu)
# Ana kanal (@handle) — Videolar sekmesi
YOUTUBE_CHANNEL_ID=
# Topic kanalı — Yayınlananlar sekmesi (ZORUNLU önerilir, otomatik arama güvenilir değil)
YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA
# Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın.
YOUTUBE_API_KEY=
# İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım)
YOUTUBE_PLAYLIST_ID=
# İsteğe bağlı: yalnızca DistroKid yayınları için açıklama filtresi
# YOUTUBE_DISTRIBUTOR_FILTER=Provided to YouTube by DistroKid
# Sadece referrer kısıtlı web anahtarı kullanıyorsanız:
# YOUTUBE_API_REFERER=https://truncgil.com
+1 -1
View File
@@ -1,4 +1,4 @@
<img width="1920" height="518" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
<img width="50%" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
# Citrus Platform
@@ -0,0 +1,118 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\BlogTopic;
use App\Models\Blog; // Ensure this model exists and has 'status' column
use App\Models\User;
use App\Services\AiBlogService;
use Illuminate\Support\Str;
use Carbon\Carbon;
class GenerateBlogAssistant extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'blog:generate-assistant {--topic= : Specific topic to generate}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a new AI blog post draft based on active topics.';
/**
* Execute the console command.
*/
public function handle(AiBlogService $aiService)
{
\Illuminate\Support\Facades\Log::info('GenerateBlogAssistant triggered. Args: ' . json_encode($_SERVER['argv'] ?? []));
$this->info('Starting AI Blog Assistant...');
// 1. Select Topic
$topicName = $this->option('topic');
$topic = null;
if ($topicName) {
$topic = BlogTopic::where('name', $topicName)->first();
if (!$topic) {
$this->error("Topic '{$topicName}' not found.");
return 1;
}
} else {
// Find oldest used active topic
$topic = BlogTopic::active()
->orderBy('last_used_at', 'asc') // Nulls first usually, or old dates
->first();
}
if (!$topic) {
$this->warn('No active topics found. Please add topics to blog_topics table.');
return 0;
}
$this->info("Selected Topic: {$topic->name}");
// 2. Generate Content
$this->info('Generating content with Gemini API...');
try {
$contentData = $aiService->generateContent($topic->name, $topic->context);
} catch (\Exception $e) {
$this->error('Content generation failed: ' . $e->getMessage());
return 1;
}
if (empty($contentData) || !isset($contentData['title']) || !isset($contentData['content'])) {
$this->error('Valid content not returned from AI Service.');
return 1;
}
$this->info('Content generated successfully.');
// 3. Generate/Fetch Image
$this->info('Fetching cover image...');
$imagePath = null;
if (isset($contentData['image_prompt'])) {
$imagePath = $aiService->fetchAndSaveImage($topic->name, $contentData['image_title'] ?? $contentData['image_prompt']); // Pollinations uses prompt
}
if (!$imagePath) {
$this->warn('Could not fetch image, proceeding without cover image.');
} else {
$this->info("Image saved to: {$imagePath}");
}
// 4. Create Draft Blog Post
$author = User::first(); // Default to first user (Admin)
$blog = new Blog();
$blog->title = $contentData['title'];
$blog->slug = Str::slug($contentData['title']);
$blog->content = $contentData['content'];
$blog->excerpt = $contentData['excerpt'] ?? Str::limit(strip_tags($contentData['content']), 150);
$blog->meta_title = $contentData['meta_title'] ?? $contentData['title'];
$blog->meta_description = $contentData['meta_description'] ?? $blog->excerpt;
$blog->status = 'draft'; // Verification required
$blog->author_id = $author ? $author->id : 1;
$blog->featured_image = $imagePath;
$blog->published_at = null; // Draft
$blog->tags = $contentData['tags'] ?? [];
// category_id? We might need to guess or default.
// For now leave null or set default if exists.
$blog->save();
// 5. Update Topic Usage
$topic->last_used_at = Carbon::now();
$topic->save();
$this->info("Blog post '{$blog->title}' created as DRAFT (ID: {$blog->id}).");
return 0;
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ class ImportHtmlTemplates extends Command
'span' => 'text.content',
'address' => 'text.address',
'button' => 'text.button',
'a' => 'url.link',
'a' => 'text.link_text',
'li' => 'text.list_item',
'label' => 'text.label',
];
@@ -260,7 +260,7 @@ class ImportHtmlTemplates extends Command
$links = $xpath->query('.//a', $node);
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (empty($href) || str_starts_with($href, '#') || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
$fixedHref = $this->fixAssetPath($href);
if (str_ends_with($fixedHref, '.html')) {
@@ -268,7 +268,7 @@ class ImportHtmlTemplates extends Command
if($fixedHref == 'index') $fixedHref = '/';
}
$key = $this->generateKey('url.link', $counts);
$key = $this->generateKey('text.link_url', $counts);
$data[$key] = $fixedHref;
$link->setAttribute('href', "{" . $key . "}");
}
@@ -0,0 +1,196 @@
<?php
namespace App\Console\Commands;
use App\Models\MusicProduction;
use App\Services\SpotifyService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class SyncSpotifyMusicProductions extends Command
{
protected $signature = 'spotify:sync-music-productions
{--artist= : Override Spotify artist ID}
{--dry-run : Show changes without writing to database}';
protected $description = 'Spotify sanatçı hesabından albüm ve single verilerini müzik prodüksiyonlarına senkronize eder';
public function handle(SpotifyService $spotifyService): int
{
if ($this->option('artist')) {
$spotifyService = new SpotifyService(
artistId: $this->option('artist'),
);
}
if (! $spotifyService->isConfigured()) {
$this->error(__('music_productions.spotify_not_configured'));
return Command::FAILURE;
}
$this->info(__('music_productions.spotify_sync_started'));
try {
$albumSummaries = $spotifyService->getArtistAlbums();
} catch (\Throwable $exception) {
$this->error($exception->getMessage());
return Command::FAILURE;
}
if (empty($albumSummaries)) {
$this->warn(__('music_productions.spotify_no_albums'));
return Command::SUCCESS;
}
$created = 0;
$updated = 0;
$dryRun = (bool) $this->option('dry-run');
foreach ($albumSummaries as $index => $albumSummary) {
$albumId = $albumSummary['id'] ?? null;
if (blank($albumId)) {
continue;
}
$album = $spotifyService->getAlbum($albumId) ?? $albumSummary;
$title = $album['name'] ?? __('music_productions.spotify_untitled_album');
$artistName = collect($album['artists'] ?? [])->pluck('name')->first();
$releaseDate = $this->parseReleaseDate($album['release_date'] ?? null, $album['release_date_precision'] ?? 'day');
$spotifyUrl = $album['external_urls']['spotify'] ?? null;
$coverUrl = collect($album['images'] ?? [])->first()['url'] ?? null;
$albumType = $album['album_type'] ?? 'album';
$production = $this->findMatchingProduction($albumId, $title);
$action = $production ? 'update' : 'create';
$this->line(sprintf(
'[%d/%d] %s: %s (%s)',
$index + 1,
count($albumSummaries),
$action === 'create' ? 'Yeni' : 'Güncelle',
$title,
$releaseDate?->format('Y-m-d') ?? '-'
));
if ($dryRun) {
continue;
}
$attributes = [
'spotify_album_id' => $albumId,
'spotify_url' => $spotifyUrl,
'spotify_cover_url' => $coverUrl,
'spotify_type' => $albumType,
'spotify_synced_at' => now(),
];
if (! $production) {
$attributes['title'] = $title;
$attributes['slug'] = SpotifyService::uniqueSlug($title);
$attributes['client_name'] = $artistName;
$attributes['production_date'] = $releaseDate;
$attributes['content'] = $spotifyService->buildDefaultContent($album);
$attributes['is_active'] = true;
$attributes['sort_order'] = 0;
$coverPath = $spotifyService->downloadCoverImage($coverUrl, $albumId);
if ($coverPath) {
$attributes['cover_image'] = $coverPath;
}
MusicProduction::create($attributes);
$created++;
continue;
}
if (blank($production->title)) {
$attributes['title'] = $title;
}
if (blank($production->client_name) && filled($artistName)) {
$attributes['client_name'] = $artistName;
}
if (blank($production->production_date) && $releaseDate) {
$attributes['production_date'] = $releaseDate;
}
if (blank($production->content)) {
$attributes['content'] = $spotifyService->buildDefaultContent($album);
}
if (blank($production->cover_image) && filled($coverUrl)) {
$coverPath = $spotifyService->downloadCoverImage($coverUrl, $albumId);
if ($coverPath) {
$attributes['cover_image'] = $coverPath;
}
}
$production->update($attributes);
$updated++;
usleep(150000);
}
$message = __('music_productions.spotify_sync_completed', [
'created' => $created,
'updated' => $updated,
'total' => count($albumSummaries),
]);
$this->info($message);
Log::info('Spotify music productions sync completed', [
'created' => $created,
'updated' => $updated,
'total' => count($albumSummaries),
'dry_run' => $dryRun,
]);
return Command::SUCCESS;
}
protected function findMatchingProduction(string $albumId, string $title): ?MusicProduction
{
$bySpotifyId = MusicProduction::withTrashed()
->where('spotify_album_id', $albumId)
->first();
if ($bySpotifyId) {
return $bySpotifyId->trashed() ? null : $bySpotifyId;
}
$slug = Str::slug($title);
$bySlug = MusicProduction::query()
->where('slug', $slug)
->first();
if ($bySlug) {
return $bySlug;
}
return MusicProduction::query()
->whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and')
->first();
}
protected function parseReleaseDate(?string $date, string $precision = 'day'): ?Carbon
{
if (blank($date)) {
return null;
}
return match ($precision) {
'year' => Carbon::createFromFormat('Y', $date)->startOfYear(),
'month' => Carbon::createFromFormat('Y-m', $date)->startOfMonth(),
default => Carbon::parse($date),
};
}
}
@@ -0,0 +1,188 @@
<?php
namespace App\Console\Commands;
use App\Models\MusicProduction;
use App\Services\YouTubeService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class SyncYoutubeReleases extends Command
{
protected $signature = 'youtube:sync-releases
{--channel= : Override YouTube main channel ID}
{--topic= : Override YouTube Topic channel ID (Yayınlananlar / art track)}
{--playlist= : Override Releases playlist ID}
{--dry-run : Show changes without writing to database}';
protected $description = 'YouTube kanalının Releases (Yayınlar) listesindeki art track kayıtlarını müzik prodüksiyonlarına senkronize eder';
public function handle(YouTubeService $youTubeService): int
{
if ($this->option('channel') || $this->option('topic') || $this->option('playlist')) {
$youTubeService = new YouTubeService(
channelId: $this->option('channel') ?? config('services.youtube.channel_id'),
topicChannelId: $this->option('topic') ?? config('services.youtube.topic_channel_id'),
playlistId: $this->option('playlist') ?? config('services.youtube.playlist_id'),
);
}
if (! $youTubeService->isConfigured()) {
$this->error(__('music_productions.youtube_not_configured'));
return Command::FAILURE;
}
$this->info(__('music_productions.youtube_sync_started'));
$this->line(__('music_productions.youtube_sync_source', [
'source' => $youTubeService->getReleasesSourceLabel(),
]));
$this->line('Playlist ID: ' . $youTubeService->resolveReleasesPlaylistId());
try {
$releases = $youTubeService->getReleases();
} catch (\Throwable $exception) {
$this->error($exception->getMessage());
return Command::FAILURE;
}
if (empty($releases)) {
$this->warn(__('music_productions.youtube_no_releases'));
return Command::SUCCESS;
}
$created = 0;
$updated = 0;
$dryRun = (bool) $this->option('dry-run');
foreach ($releases as $index => $release) {
$videoId = $release['video_id'] ?? null;
if (blank($videoId)) {
continue;
}
$title = $release['title'] ?? __('music_productions.youtube_untitled_release');
$releaseDate = filled($release['release_date'] ?? null)
? Carbon::parse($release['release_date'])
: null;
$youtubeUrl = $release['youtube_url'] ?? null;
$coverUrl = $release['cover_url'] ?? null;
$description = $release['description'] ?? '';
$production = $this->findMatchingProduction($videoId, $title);
$action = $production ? 'update' : 'create';
$this->line(sprintf(
'[%d/%d] %s: %s (%s)',
$index + 1,
count($releases),
$action === 'create' ? 'Yeni' : 'Güncelle',
$title,
$releaseDate?->format('Y-m-d') ?? '-'
));
if ($dryRun) {
continue;
}
$attributes = [
'youtube_video_id' => $videoId,
'youtube_url' => $youtubeUrl,
'youtube_cover_url' => $coverUrl,
'youtube_description' => $description,
'youtube_synced_at' => now(),
];
if (! $production) {
$attributes['title'] = $title;
$attributes['slug'] = YouTubeService::uniqueSlug($title);
$attributes['production_date'] = $releaseDate;
$attributes['content'] = $youTubeService->buildDefaultContent($release);
$attributes['is_active'] = true;
$attributes['sort_order'] = 0;
$coverPath = $youTubeService->downloadCoverImage($coverUrl, $videoId);
if ($coverPath) {
$attributes['cover_image'] = $coverPath;
}
MusicProduction::create($attributes);
$created++;
continue;
}
if (blank($production->title)) {
$attributes['title'] = $title;
}
if ($releaseDate) {
$attributes['production_date'] = $releaseDate;
}
if (blank($production->content)) {
$attributes['content'] = $youTubeService->buildDefaultContent($release);
}
if (blank($production->cover_image) && filled($coverUrl)) {
$coverPath = $youTubeService->downloadCoverImage($coverUrl, $videoId);
if ($coverPath) {
$attributes['cover_image'] = $coverPath;
}
}
$production->update($attributes);
$updated++;
usleep(150000);
}
$message = __('music_productions.youtube_sync_completed', [
'created' => $created,
'updated' => $updated,
'total' => count($releases),
]);
$this->info($message);
Log::info('YouTube releases sync completed', [
'created' => $created,
'updated' => $updated,
'total' => count($releases),
'dry_run' => $dryRun,
]);
return Command::SUCCESS;
}
protected function findMatchingProduction(string $videoId, string $title): ?MusicProduction
{
$byYoutubeId = MusicProduction::withTrashed()
->where('youtube_video_id', $videoId)
->first();
if ($byYoutubeId) {
return $byYoutubeId->trashed() ? null : $byYoutubeId;
}
$slug = Str::slug($title);
$bySlug = MusicProduction::query()
->whereNull('youtube_video_id', 'and', false)
->where('slug', '=', $slug)
->first();
if ($bySlug) {
return $bySlug;
}
return MusicProduction::query()
->whereNull('youtube_video_id', 'and', false)
->whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and')
->first();
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Filament\Admin\Resources\Awards;
use App\Filament\Admin\Resources\Awards\Pages\CreateAward;
use App\Filament\Admin\Resources\Awards\Pages\EditAward;
use App\Filament\Admin\Resources\Awards\Pages\ListAwards;
use App\Filament\Admin\Resources\Awards\Schemas\AwardForm;
use App\Filament\Admin\Resources\Awards\Tables\AwardsTable;
use App\Models\Award;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class AwardResource extends Resource
{
protected static ?string $model = Award::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-trophy';
protected static ?int $navigationSort = 46;
public static function getNavigationGroup(): ?string
{
return __('awards.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('awards.navigation_label');
}
public static function getModelLabel(): string
{
return __('awards.model_label');
}
public static function getPluralModelLabel(): string
{
return __('awards.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return AwardForm::configure($schema);
}
public static function table(Table $table): Table
{
return AwardsTable::configure($table);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => ListAwards::route('/'),
'create' => CreateAward::route('/create'),
'edit' => EditAward::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Filament\Admin\Resources\Awards\Pages;
use App\Filament\Admin\Resources\Awards\AwardResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Resources\Pages\CreateRecord;
class CreateAward extends CreateRecord
{
protected static string $resource = AwardResource::class;
public function getTitle(): string
{
return __('awards.create') ?? 'Yeni Ödül Ekle';
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getCreatedNotificationTitle(): ?string
{
return __('awards.created_successfully') ?? 'Ödül başarıyla eklendi.';
}
protected function afterCreate(): void
{
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Filament\Admin\Resources\Awards\Pages;
use App\Filament\Admin\Resources\Awards\AwardResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditAward extends EditRecord
{
protected static string $resource = AwardResource::class;
public function getTitle(): string
{
return __('awards.edit') ?? 'Ödülü Düzenle';
}
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->label(__('awards.delete') ?? 'Sil'),
ForceDeleteAction::make()
->label(__('awards.force_delete') ?? 'Kalıcı Olarak Sil'),
RestoreAction::make()
->label(__('awards.restore') ?? 'Geri Yükle'),
];
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getSavedNotificationTitle(): ?string
{
return __('awards.updated_successfully') ?? 'Ödül başarıyla güncellendi.';
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function afterSave(): void
{
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Admin\Resources\Awards\Pages;
use App\Filament\Admin\Resources\Awards\AwardResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListAwards extends ListRecords
{
protected static string $resource = AwardResource::class;
public function getTitle(): string
{
return __('awards.title') ?? 'Ödüllerimiz';
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label(__('awards.create') ?? 'Yeni Ödül Ekle'),
];
}
}
@@ -0,0 +1,117 @@
<?php
namespace App\Filament\Admin\Resources\Awards\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class AwardForm
{
public static function categoryOptions(): array
{
return [
'hackathon' => __('awards.category_hackathon') ?? 'Hackathon',
'export' => __('awards.category_export') ?? 'İhracat',
'innovation' => __('awards.category_innovation') ?? 'İnovasyon',
'design' => __('awards.category_design') ?? 'Tasarım',
'general' => __('awards.category_general') ?? 'Genel',
];
}
public static function configure(Schema $schema): Schema
{
return $schema
->columns(3)
->schema([
Section::make(__('awards.content_section') ?? 'Ödül İçeriği')
->schema([
TextInput::make('title')
->label(__('awards.title_field') ?? 'Ödül Başlığı (Varsayılan)')
->required()
->maxLength(255),
TextInput::make('issuer')
->label(__('awards.issuer_field') ?? 'Ödülü Veren Kurum (Varsayılan)')
->required()
->maxLength(255),
Textarea::make('description')
->label(__('awards.description_field') ?? 'Açıklama (Varsayılan)')
->required()
->rows(5)
->columnSpanFull(),
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('awards.title_field') ?? 'Ödül Başlığı',
'required' => false,
'maxLength' => 255,
],
'issuer' => [
'type' => 'text',
'label' => __('awards.issuer_field') ?? 'Ödülü Veren Kurum',
'required' => false,
'maxLength' => 255,
],
'description' => [
'type' => 'textarea',
'label' => __('awards.description_field') ?? 'Açıklama',
'required' => false,
'rows' => 5,
],
]),
])
->columnSpan(2),
Section::make(__('awards.settings_section') ?? 'Ödül Ayarları')
->schema([
FileUpload::make('image')
->label(__('awards.image_field') ?? 'Ödül Görseli / Logo')
->image()
->disk('public')
->directory('awards')
->required(),
DatePicker::make('award_date')
->label(__('awards.date_field') ?? 'Ödül Tarihi')
->required(),
Select::make('category')
->label(__('awards.category_field') ?? 'Kategori')
->options(self::categoryOptions())
->default('general')
->required()
->native(false),
TextInput::make('external_link')
->label(__('awards.link_field') ?? 'Doğrulama / Haber Linki')
->url()
->maxLength(255),
TextInput::make('sort_order')
->label(__('awards.sort_order_field') ?? 'Sıralama')
->numeric()
->default(0)
->minValue(0),
Toggle::make('is_featured')
->label(__('awards.is_featured_field') ?? 'Öne Çıkarılan Ödül')
->default(false),
Toggle::make('is_active')
->label(__('awards.is_active_field') ?? 'Aktif / Görünür')
->default(true),
])
->columnSpan(1),
]);
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Filament\Admin\Resources\Awards\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class AwardsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
ImageColumn::make('image')
->label(__('awards.table_image') ?? 'Logo')
->disk('public')
->square()
->size(50),
TextColumn::make('title')
->label(__('awards.table_title') ?? 'Ödül Başlığı')
->searchable()
->sortable()
->limit(40),
TextColumn::make('issuer')
->label(__('awards.table_issuer') ?? 'Veren Kurum')
->searchable()
->sortable()
->limit(30),
TextColumn::make('award_date')
->label(__('awards.table_date') ?? 'Ödül Tarihi')
->date('d.m.Y')
->sortable()
->alignCenter(),
TextColumn::make('category')
->label(__('awards.table_category') ?? 'Kategori')
->badge()
->formatStateUsing(fn (string $state): string => match ($state) {
'hackathon' => __('awards.category_hackathon') ?? 'Hackathon',
'export' => __('awards.category_export') ?? 'İhracat',
'innovation' => __('awards.category_innovation') ?? 'İnovasyon',
'design' => __('awards.category_design') ?? 'Tasarım',
default => __('awards.category_general') ?? 'Genel',
})
->sortable()
->alignCenter(),
ToggleColumn::make('is_featured')
->label(__('awards.table_is_featured') ?? 'Öne Çıkan')
->alignCenter(),
ToggleColumn::make('is_active')
->label(__('awards.table_is_active') ?? 'Aktif')
->alignCenter(),
TextColumn::make('sort_order')
->label(__('awards.sort_order_field') ?? 'Sıra')
->sortable()
->alignCenter(),
])
->filters([
TernaryFilter::make('is_active')
->label(__('awards.is_active_field') ?? 'Aktiflik Durumu'),
TernaryFilter::make('is_featured')
->label(__('awards.is_featured_field') ?? 'Öne Çıkarılma Durumu'),
TrashedFilter::make(),
])
->recordActions([
EditAction::make()
->label(__('awards.edit') ?? 'Düzenle'),
])
->actions([
// Individual actions can be put here if necessary
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->label(__('awards.delete') ?? 'Sil'),
RestoreBulkAction::make()
->label(__('awards.restore') ?? 'Geri Yükle'),
ForceDeleteBulkAction::make()
->label(__('awards.force_delete') ?? 'Kalıcı Olarak Sil'),
]),
])
->defaultSort('sort_order', 'asc')
->reorderable('sort_order');
}
}
@@ -21,7 +21,7 @@ class BlogCategoryResource extends Resource
{
protected static ?string $model = BlogCategory::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';
protected static UnitEnum|string|null $navigationGroup = 'Blog';
@@ -20,7 +20,7 @@ class BlogResource extends Resource
{
protected static ?string $model = Blog::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
public static function getNavigationLabel(): string
{
@@ -0,0 +1,77 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems;
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\CreateCompanyHistoryItem;
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\EditCompanyHistoryItem;
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\ListCompanyHistoryItems;
use App\Filament\Admin\Resources\CompanyHistoryItems\Schemas\CompanyHistoryItemForm;
use App\Filament\Admin\Resources\CompanyHistoryItems\Tables\CompanyHistoryItemsTable;
use App\Models\CompanyHistoryItem;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class CompanyHistoryItemResource extends Resource
{
protected static ?string $model = CompanyHistoryItem::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clock';
protected static ?int $navigationSort = 45;
public static function getNavigationGroup(): ?string
{
return __('company_history.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('company_history.navigation_label');
}
public static function getModelLabel(): string
{
return __('company_history.model_label');
}
public static function getPluralModelLabel(): string
{
return __('company_history.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return CompanyHistoryItemForm::configure($schema);
}
public static function table(Table $table): Table
{
return CompanyHistoryItemsTable::configure($table);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => ListCompanyHistoryItems::route('/'),
'create' => CreateCompanyHistoryItem::route('/create'),
'edit' => EditCompanyHistoryItem::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Resources\Pages\CreateRecord;
class CreateCompanyHistoryItem extends CreateRecord
{
protected static string $resource = CompanyHistoryItemResource::class;
public function getTitle(): string
{
return __('company_history.create');
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getCreatedNotificationTitle(): ?string
{
return __('company_history.created_successfully');
}
protected function mutateFormDataBeforeCreate(array $data): array
{
if (empty($data['position'])) {
$data['position'] = null;
}
unset($data['color_custom']);
return $data;
}
protected function afterCreate(): void
{
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditCompanyHistoryItem extends EditRecord
{
protected static string $resource = CompanyHistoryItemResource::class;
public function getTitle(): string
{
return __('company_history.edit');
}
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->label(__('company_history.delete')),
ForceDeleteAction::make()
->label(__('company_history.force_delete')),
RestoreAction::make()
->label(__('company_history.restore')),
];
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getSavedNotificationTitle(): ?string
{
return __('company_history.updated_successfully');
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (empty($data['position'])) {
$data['position'] = null;
}
unset($data['color_custom']);
return $data;
}
protected function afterSave(): void
{
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCompanyHistoryItems extends ListRecords
{
protected static string $resource = CompanyHistoryItemResource::class;
public function getTitle(): string
{
return __('company_history.title');
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label(__('company_history.create')),
];
}
}
@@ -0,0 +1,141 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class CompanyHistoryItemForm
{
public static function iconOptions(): array
{
return [
'heroicon-o-building-office-2' => __('company_history.icon_building'),
'heroicon-o-academic-cap' => __('company_history.icon_academic'),
'heroicon-o-globe-alt' => __('company_history.icon_globe'),
'heroicon-o-users' => __('company_history.icon_users'),
'heroicon-o-trophy' => __('company_history.icon_trophy'),
'heroicon-o-map' => __('company_history.icon_map'),
'heroicon-o-light-bulb' => __('company_history.icon_light_bulb'),
'heroicon-o-rocket-launch' => __('company_history.icon_rocket'),
];
}
public static function quarterOptions(): array
{
return [
'Q1' => __('company_history.quarter_q1'),
'Q2' => __('company_history.quarter_q2'),
'Q3' => __('company_history.quarter_q3'),
'Q4' => __('company_history.quarter_q4'),
];
}
public static function colorPresets(): array
{
return [
'#17a2b8' => __('company_history.color_teal'),
'#f5a623' => __('company_history.color_orange'),
'#e91e63' => __('company_history.color_pink'),
'#2c3e50' => __('company_history.color_navy'),
'#5dade2' => __('company_history.color_blue'),
'#27ae60' => __('company_history.color_green'),
];
}
public static function configure(Schema $schema): Schema
{
return $schema
->columns(3)
->schema([
Section::make(__('company_history.content_section'))
->schema([
TextInput::make('year')
->label(__('company_history.year_field'))
->required()
->numeric()
->minValue(1900)
->maxValue(2100),
Select::make('quarter')
->label(__('company_history.quarter_field'))
->options(self::quarterOptions())
->default('Q1')
->required()
->native(false),
TextInput::make('title')
->label(__('company_history.title_field'))
->required()
->maxLength(255),
Textarea::make('content')
->label(__('company_history.content_field'))
->required()
->rows(5)
->columnSpanFull(),
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('company_history.title_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'textarea',
'label' => __('company_history.content_field'),
'required' => false,
'rows' => 5,
],
]),
])
->columnSpan(2)
->collapsible(false),
Section::make(__('company_history.settings_section'))
->schema([
Select::make('color')
->label(__('company_history.color_field'))
->options(self::colorPresets())
->default('#17a2b8')
->required()
->native(false)
->searchable(),
Select::make('icon')
->label(__('company_history.icon_field'))
->options(self::iconOptions())
->default('heroicon-o-building-office-2')
->required()
->native(false)
->searchable(),
Select::make('position')
->label(__('company_history.position_field'))
->options([
'' => __('company_history.position_auto'),
'left' => __('company_history.position_left'),
'right' => __('company_history.position_right'),
])
->default(null),
TextInput::make('sort_order')
->label(__('company_history.sort_order_field'))
->numeric()
->default(0)
->minValue(0),
Checkbox::make('is_active')
->label(__('company_history.is_active_field'))
->default(true),
])
->columnSpan(1),
]);
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\ColorColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
class CompanyHistoryItemsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('year')
->label(__('company_history.table_year'))
->sortable()
->alignCenter(),
TextColumn::make('quarter')
->label(__('company_history.quarter_field'))
->badge()
->sortable()
->alignCenter(),
ColorColumn::make('color')
->label(__('company_history.color_field')),
TextColumn::make('title')
->label(__('company_history.table_title'))
->searchable()
->sortable()
->limit(40),
TextColumn::make('content')
->label(__('company_history.content_field'))
->limit(60)
->toggleable(),
TextColumn::make('position')
->label(__('company_history.position_field'))
->formatStateUsing(fn (?string $state): string => match ($state) {
'left' => __('company_history.position_left'),
'right' => __('company_history.position_right'),
default => __('company_history.position_auto'),
})
->toggleable(),
ToggleColumn::make('is_active')
->label(__('company_history.table_is_active'))
->alignCenter(),
TextColumn::make('sort_order')
->label(__('company_history.sort_order_field'))
->sortable()
->alignCenter(),
TextColumn::make('created_at')
->label(__('company_history.table_created_at'))
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label(__('company_history.table_updated_at'))
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TernaryFilter::make('is_active')
->label(__('company_history.is_active_field')),
TrashedFilter::make(),
])
->recordActions([
EditAction::make()
->label(__('company_history.edit')),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->label(__('company_history.delete')),
RestoreBulkAction::make()
->label(__('company_history.restore')),
ForceDeleteBulkAction::make()
->label(__('company_history.force_delete')),
]),
])
->defaultSort('year', 'asc')
->reorderable('sort_order');
}
}
@@ -0,0 +1,519 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications;
use App\Models\CareerApplication;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Livewire;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Database\Eloquent\Builder;
class InternApplicationResource extends Resource
{
protected static ?string $model = CareerApplication::class;
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-academic-cap';
public static function getNavigationLabel(): string
{
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
}
public static function getModelLabel(): string
{
return __('career.internship', ['default' => 'Staj Başvurusu']);
}
public static function getPluralModelLabel(): string
{
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->where('type', 'internship');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make('Tabs')
->tabs([
Tab::make('Kişisel ve Başvuru Bilgileri')
->icon('heroicon-m-user')
->schema([
TextInput::make('name')
->label(__('career.name'))
->required()
->disabled(),
TextInput::make('email')
->label(__('career.email'))
->email()
->required()
->disabled(),
TextInput::make('phone')
->label(__('career.phone'))
->disabled(),
Select::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
])
->required(),
Textarea::make('message')
->label(__('career.message'))
->disabled()
->columnSpanFull(),
])->columns(2),
Tab::make('Staj Belgeleri & Giriş Bilgileri')
->icon('heroicon-m-document-text')
->schema([
FileUpload::make('cv_path')
->label(__('career.cv'))
->disk('public')
->directory('cvs')
->required()
->disabled()
->downloadable(),
TextInput::make('username')
->label('Kullanıcı Adı')
->default(fn ($record) => $record?->email)
->disabled()
->dehydrated()
->autocomplete('new-username'),
TextInput::make('password')
->label('Şifre')
->password()
->revealable()
->autocomplete('new-password')
->formatStateUsing(fn () => null)
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
->dehydrated(fn ($state) => filled($state))
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
->nullable()
->suffixAction(
\Filament\Actions\Action::make('generatePassword')
->icon('heroicon-m-arrow-path')
->action(fn (Set $set) => $set('password', Str::random(12)))
),
FileUpload::make('to_be_signed_internship_form_path')
->label('İmzalanacak Staj Formu (Stajyerden)')
->disk('public')
->directory('to_be_signed_interns')
->downloadable()
->nullable(),
FileUpload::make('signed_internship_form_path')
->label('İmzalı Staj Formu')
->disk('public')
->directory('signed_interns')
->downloadable()
->live()
->afterStateUpdated(function ($state, Set $set) {
if ($state) {
$set('status', 'accepted');
}
})
->nullable(),
DatePicker::make('internship_start_date')
->label('Staj Başlangıç Tarihi')
->live()
->afterStateUpdated(function ($state, $get, Set $set) {
if ($state && $get('internship_total_days')) {
self::calculateEndDate($state, $get('internship_total_days'), $set);
} elseif ($state && $get('internship_end_date')) {
self::calculateTotalDays($state, $get('internship_end_date'), $set);
}
})
->nullable(),
DatePicker::make('internship_end_date')
->label('Staj Bitiş Tarihi')
->live()
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
->nullable(),
TextInput::make('internship_total_days')
->label('Toplam Staj Süresi (İş Günü)')
->numeric()
->live()
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
->nullable(),
])->columns(2),
Tab::make('Staj Günlüğü')
->icon('heroicon-m-squares-plus')
->schema([
TextInput::make('github_repo')
->label('GitHub Depo URL\'si')
->url()
->nullable()
->live(),
Livewire::make(\App\Livewire\InternJournalTimeline::class)
->columnSpanFull()
]),
Tab::make('Staj Defteri & Onay')
->icon('heroicon-o-book-open')
->schema([
\Filament\Forms\Components\Placeholder::make('notebook_view')
->label('Doldurulan Staj Defteri')
->content(function ($record) {
if (!$record) return 'Henüz başvuru bulunmamaktadır.';
$days = \App\Http\Controllers\CareerController::getInternshipDates($record->internship_start_date, $record->internship_total_days);
if (empty($days)) return 'Staj başlangıç tarihi veya süresi girilmemiş.';
$saved = $record->journalEntries()->get()->keyBy('day_number');
$html = '<style>
.rich-text-content p { margin-bottom: 8px; }
.rich-text-content ul { list-style-type: disc; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content ol { list-style-type: decimal; padding-left: 20px; margin-bottom: 8px; }
.rich-text-content li { margin-bottom: 4px; }
</style>';
$html .= '<div class="space-y-4" style="max-height: 400px; overflow-y: auto; padding-right: 10px; border: 1px solid #cbd5e1; border-radius: 8px; padding: 15px;">';
foreach ($days as $d) {
$dayNum = $d['day_number'];
$dateF = $d['formatted_date'];
$entry = $saved->get($dayNum);
$content = $entry ? $entry->content : '';
$isRetro = $entry ? $entry->is_retroactive : false;
$updatedAt = $entry ? $entry->updated_at->format('d.m.Y H:i') : null;
$html .= '<div style="margin-bottom: 12px; padding: 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc;">';
$html .= ' <div style="display:flex; justify-content:between; font-size:11px; font-weight:700; color:#475569; border-bottom:1px solid #e2e8f0; padding-bottom:6px; margin-bottom:8px;">';
$html .= ' <span style="font-weight: 800; color: #2563eb;">' . $dayNum . '. Gün Raporu</span>';
if ($isRetro) {
$html .= ' <span style="margin-left: 10px; background: #fee2e2; color: #991b1b; padding: 1px 6px; border-radius: 4px; font-size: 9px; font-weight: 800;">GERİYE DÖNÜK KAYIT</span>';
}
if ($updatedAt) {
$html .= ' <span style="margin-left: auto;">Son Güncelleme: ' . $updatedAt . '</span>';
} else {
$html .= ' <span style="margin-left: auto;">' . $dateF . '</span>';
}
$html .= ' </div>';
$cleanContent = $content ? strip_tags($content, ['p', 'strong', 'ul', 'li', 'em', 'br', 'b', 'i', 'ol', 'span']) : '<em style="color:#94a3b8;">Rapor yazılmamış</em>';
$html .= ' <div class="rich-text-content" style="font-size:12px; color:#1e293b; line-height:1.5;">' . $cleanContent . '</div>';
if ($entry && trim($content) !== '') {
$supApproved = $entry->supervisor_approved;
$supName = $entry->supervisor_name;
$html .= ' <div style="display:flex; align-items:center; gap:12px; margin-top:12px; padding-top:10px; border-top:1px dashed #e2e8f0; font-size:11px;">';
// Supervisor approval
$supBg = $supApproved ? '#d1fae5' : '#f1f5f9';
$supColor = $supApproved ? '#065f46' : '#64748b';
$supText = $supApproved ? 'Sorumlu Onayladı' . ($supName ? ' (Onaylayan: ' . e($supName) . ')' : '') : 'Sorumlu Onayı Bekliyor';
$html .= ' <span id="sup-badge-' . $entry->id . '" style="background:' . $supBg . '; color:' . $supColor . '; padding: 2px 8px; border-radius: 4px; font-weight: 700;">' . $supText . '</span>';
// Buttons
$supBtnText = $supApproved ? 'Onayı Kaldır' : 'Onayla';
$supBtnBg = $supApproved ? '#ef4444' : '#2563eb';
$html .= ' <button type="button" onclick="toggleApproval(' . $entry->id . ', this)" style="margin-left:auto; background:' . $supBtnBg . '; color:white; border:none; padding:4px 10px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:10px;">Sorumlu ' . $supBtnText . '</button>';
$html .= ' </div>';
}
$html .= '</div>';
}
$html .= '</div>';
// JS handler
$html .= '
<script>
if (typeof window.toggleApproval !== "function") {
window.toggleApproval = function(entryId, btn) {
btn.disabled = true;
btn.style.opacity = "0.5";
fetch("' . route('intern.admin.toggle-journal-approval') . '", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "' . csrf_token() . '"
},
body: JSON.stringify({
entry_id: entryId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const badge = document.getElementById("sup-badge-" + entryId);
if (data.status) {
badge.style.background = "#d1fae5";
badge.style.color = "#065f46";
badge.textContent = "Sorumlu Onayladı (Onaylayan: " + data.supervisor_name + ")";
btn.textContent = "Sorumlu Onayı Kaldır";
btn.style.background = "#ef4444";
} else {
badge.style.background = "#f1f5f9";
badge.style.color = "#64748b";
badge.textContent = "Sorumlu Onayı Bekliyor";
btn.textContent = "Sorumlu Onayla";
btn.style.background = "#2563eb";
}
} else {
alert(data.message || "Bir hata oluştu.");
}
})
.catch(err => {
console.error(err);
alert("Bağlantı hatası oluştu.");
})
.finally(() => {
btn.disabled = false;
btn.style.opacity = "1";
});
};
}
</script>
';
// Add preview buttons
$html .= '<div style="margin-top: 15px; display: flex; gap: 10px;">';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a4&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#2563eb; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(37, 99, 235, 0.2);">A4 Defteri Önizle / Yazdır</a>';
$html .= ' <a href="' . route('intern.print-journal') . '?size=a5&intern_id=' . $record->id . '" target="_blank" style="display:inline-flex; align-items:center; justify-content:center; padding: 8px 16px; background:#4b5563; color:white; border-radius:8px; font-weight:bold; font-size:12px; text-decoration:none; box-shadow: 0 1px 3px rgba(75, 85, 99, 0.2);">A5 Defteri Önizle / Yazdır</a>';
$html .= '</div>';
return new \Illuminate\Support\HtmlString($html);
})
->columnSpanFull(),
\Filament\Schemas\Components\Section::make('Onay ve İmza Bilgileri')
->schema([
\Filament\Forms\Components\Toggle::make('notebook_supervisor_signed')
->label('Staj Sorumlusu İmzala / Onayla')
->live(),
TextInput::make('notebook_supervisor_name')
->label('Staj Sorumlusu Adı / Ünvanı')
->placeholder('Örn: Alperen Trunç')
->default('Alperen Trunç'),
\Filament\Forms\Components\Toggle::make('notebook_approved')
->label('Staj Defterini Genel Olarak Onayla')
->columnSpanFull(),
])->columns(2),
]),
Tab::make('Sertifika & Transkript')
->icon('heroicon-o-academic-cap')
->schema([
TextInput::make('certificate_code')
->label('Doğrulama Kodu')
->helperText('Belge kaydedildiğinde benzersiz doğrulama kodu otomatik olarak üretilir.')
->readonly()
->nullable(),
MarkdownEditor::make('transcript_markdown')
->label('Akademik Transkript (Markdown)')
->columnSpanFull()
->default(function () {
return "### STAJ AKADEMİK TRANSKRİPTİ VE PERFORMANS RAPORU\n\n" .
"#### 🛠️ Deneyimlenen Teknolojiler ve Kazanımlar\n" .
"| Modül / Çalışma Alanı | Kullanılan Teknolojiler / Araçlar | Değerlendirme |\n" .
"| --- | --- | --- |\n" .
"| Backend Mimari & API | Laravel framework, RESTful API, MySQL | Başarılı |\n" .
"| Arayüz & UI/UX Uygulamaları | Flutter, CSS, Glassmorphic Tasarım Prensipleri | Üstün Başarı |\n" .
"| Masaüstü & Sistem Entegrasyonu | Electron.js, Git / GitHub | Başarılı |\n" .
"| Takım Çalışması & Proje Yönetimi | Agile / Scrum, Slack, JIRA | Başarılı |\n\n" .
"#### 📊 Performans Değerlendirme Kriterleri\n" .
"| Değerlendirme Kriteri | Puan (100 Üzerinden) | Harf Notu |\n" .
"| --- | --- | --- |\n" .
"| Teknik Sorumluluk ve Görev Bilinci | 95 | AA |\n" .
"| Problem Çözme ve Analitik Düşünme | 90 | BA |\n" .
"| Ekip Çalışması ve İletişim Uyum | 95 | AA |\n" .
"| Öğrenme Hızı ve Adaptasyon | 100 | AA |\n" .
"| **GENEL BAŞARI ORTALAMASI** | **95.00** | **AA (Mükemmel)** |\n\n" .
"#### 📝 Danışman Görüşü ve Değerlendirme Notu\n" .
"\"Stajyerimiz, staj süresi boyunca kendisine verilen görevleri büyük bir titizlikle yerine getirmiştir. Özellikle karşılaştığı teknik problemlere getirdiği pratik çözümler ve yeni teknolojileri öğrenme isteği takdir edilmeye değerdir. Kurumumuz bünyesinde yürüttüğümüz projelere sağladığı katkılardan ötürü teşekkür eder, profesyonel kariyerinde başarılar dileriz.\"";
}),
])->columns(1)
])->columnSpanFull()
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('career.name'))
->searchable()
->sortable(),
TextColumn::make('email')
->label(__('career.email'))
->searchable()
->sortable(),
TextColumn::make('phone')
->label(__('career.phone'))
->searchable(),
TextColumn::make('status')
->label(__('career.status'))
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'gray',
'reviewed' => 'info',
'rejected' => 'danger',
'accepted' => 'success',
'waiting_document' => 'warning',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
TextColumn::make('certificate_code')
->label('Sertifika Kodu')
->searchable()
->placeholder('Yok'),
TextColumn::make('created_at')
->label(__('career.created_at'))
->dateTime('d.m.Y H:i')
->sortable(),
])
->filters([
SelectFilter::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
]),
])
->actions([
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
Action::make('download_signed_form')
->label('İmzalı Form İndir')
->icon('heroicon-o-document-check')
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
->visible(fn ($record) => !empty($record->signed_internship_form_path))
->openUrlInNewTab(),
Action::make('view_certificate')
->label('Sertifika Doğrulama')
->icon('heroicon-o-academic-cap')
->color('success')
->url(fn ($record) => $record->certificate_code ? route('internship.verify', $record->certificate_code) : null)
->visible(fn ($record) => !empty($record->certificate_code))
->openUrlInNewTab(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
public static function calculateTotalDays($start, $end, Set $set): void
{
if (!$start || !$end) {
$set('internship_total_days', null);
return;
}
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
if ($startDate->gt($endDate)) {
$set('internship_total_days', 0);
return;
}
$days = 0;
while ($startDate->lte($endDate)) {
if (!$startDate->isWeekend()) {
$days++;
}
$startDate->addDay();
}
$set('internship_total_days', $days);
}
public static function calculateEndDate($start, $totalDays, Set $set): void
{
if (!$start || !$totalDays || $totalDays <= 0) {
return;
}
$startDate = \Carbon\Carbon::parse($start);
$daysToAdd = intval($totalDays);
$endDate = $startDate->copy();
$count = 0;
$temp = $startDate->copy();
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
$temp->addDay();
continue;
}
$endDate = $temp->copy();
$temp->addDay();
$count++;
}
$set('internship_end_date', $endDate->format('Y-m-d'));
}
public static function getPages(): array
{
return [
'index' => Pages\ListInternApplications::route('/'),
'create' => Pages\CreateInternApplication::route('/create'),
'edit' => Pages\EditInternApplication::route('/{record}/edit'),
];
}
public static function getWidgets(): array
{
return [
Widgets\InternGanttChartWidget::class,
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications\Pages;
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateInternApplication extends CreateRecord
{
protected static string $resource = InternApplicationResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications\Pages;
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditInternApplication extends EditRecord
{
protected static string $resource = InternApplicationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications\Pages;
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
use Filament\Resources\Pages\ListRecords;
class ListInternApplications extends ListRecords
{
protected static string $resource = InternApplicationResource::class;
protected function getHeaderWidgets(): array
{
return [
\App\Filament\Admin\Resources\InternApplications\Widgets\InternGanttChartWidget::class,
];
}
}
@@ -0,0 +1,175 @@
<?php
namespace App\Filament\Admin\Resources\InternApplications\Widgets;
use App\Models\CareerApplication;
use Filament\Widgets\Widget;
use Carbon\Carbon;
class InternGanttChartWidget extends Widget
{
protected string $view = 'filament.admin.resources.intern-applications.widgets.intern-gantt-chart-widget';
protected int | string | array $columnSpan = 'full';
public function getViewData(): array
{
$interns = CareerApplication::query()
->where('type', 'internship')
->where('status', 'accepted')
->whereNotNull('internship_start_date')
->whereNotNull('internship_end_date')
->orderBy('internship_start_date', 'asc')
->get();
if ($interns->isEmpty()) {
return [
'interns' => [],
'months' => [],
'totalDays' => 0,
'minDate' => null,
'todayPercent' => null,
];
}
$minDate = Carbon::parse($interns->min('internship_start_date'))->startOfDay();
$maxDate = Carbon::parse($interns->max('internship_end_date'))->endOfDay();
// Pad the range slightly for better visual presentation
$minDate = $minDate->copy()->subDays(5);
$maxDate = $maxDate->copy()->addDays(5);
$totalDays = $minDate->diffInDays($maxDate);
if ($totalDays <= 0) {
$totalDays = 1;
}
// Generate months in range
$months = [];
$tempDate = $minDate->copy()->startOfMonth();
$endLimit = $maxDate->copy()->endOfMonth();
while ($tempDate->lte($endLimit)) {
$monthStart = $tempDate->copy()->startOfMonth();
$monthEnd = $tempDate->copy()->endOfMonth();
$overlapStart = $monthStart->gt($minDate) ? $monthStart : $minDate;
$overlapEnd = $monthEnd->lt($maxDate) ? $monthEnd : $maxDate;
if ($overlapStart->lte($overlapEnd)) {
$daysInOverlap = $overlapStart->diffInDays($overlapEnd) + 1;
$startOffset = $minDate->diffInDays($overlapStart);
$months[] = [
'name' => $tempDate->translatedFormat('F Y'),
'left' => ($startOffset / $totalDays) * 100,
'width' => ($daysInOverlap / $totalDays) * 100,
];
}
$tempDate->addMonth();
}
// Calculate today's line position if it falls within the range
$today = Carbon::today();
$todayPercent = null;
if ($today->between($minDate, $maxDate)) {
$todayPercent = ($minDate->diffInDays($today) / $totalDays) * 100;
}
$formattedInterns = $interns->map(function ($intern) use ($minDate, $totalDays) {
$start = Carbon::parse($intern->internship_start_date)->startOfDay();
$end = Carbon::parse($intern->internship_end_date)->endOfDay();
$startOffset = $minDate->diffInDays($start);
$duration = $start->diffInDays($end) + 1;
$left = ($startOffset / $totalDays) * 100;
$width = ($duration / $totalDays) * 100;
// Ensure width is at least a visible sliver
if ($width < 1.5) {
$width = 1.5;
}
// Assign a stable color palette based on the intern ID
$palettes = [
[
'bg' => 'bg-emerald-500 dark:bg-emerald-600',
'gradient' => 'from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-teal-600',
'text' => 'text-emerald-950 dark:text-emerald-50',
'border' => 'border-emerald-500/20',
'shadow' => 'shadow-emerald-500/20'
],
[
'bg' => 'bg-blue-500 dark:bg-blue-600',
'gradient' => 'from-blue-400 to-indigo-500 dark:from-blue-500 dark:to-indigo-600',
'text' => 'text-blue-950 dark:text-blue-50',
'border' => 'border-blue-500/20',
'shadow' => 'shadow-blue-500/20'
],
[
'bg' => 'bg-rose-500 dark:bg-rose-600',
'gradient' => 'from-rose-400 to-pink-500 dark:from-rose-500 dark:to-pink-600',
'text' => 'text-rose-950 dark:text-rose-50',
'border' => 'border-rose-500/20',
'shadow' => 'shadow-rose-500/20'
],
[
'bg' => 'bg-amber-500 dark:bg-amber-600',
'gradient' => 'from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600',
'text' => 'text-amber-950 dark:text-amber-50',
'border' => 'border-amber-500/20',
'shadow' => 'shadow-amber-500/20'
],
[
'bg' => 'bg-violet-500 dark:bg-violet-600',
'gradient' => 'from-violet-400 to-purple-500 dark:from-violet-500 dark:to-purple-600',
'text' => 'text-violet-950 dark:text-violet-50',
'border' => 'border-violet-500/20',
'shadow' => 'shadow-violet-500/20'
],
];
$paletteIndex = $intern->id % count($palettes);
$color = $palettes[$paletteIndex];
$startMonth = (int) $start->format('n');
$endMonth = (int) $end->format('n');
$gridStart = $startMonth + 1;
$gridEnd = min(14, $endMonth + 2);
// Background color map based on user's examples
$hexColors = [
'#2ecaac', // Emerald Teal
'#54c6f9', // Sky Blue
'#ff6252', // Red Coral
'#f59e0b', // Amber
'#8b5cf6', // Violet
];
$hexColor = $hexColors[$intern->id % count($hexColors)];
$isStripes = ($intern->id % 2 === 0);
return [
'id' => $intern->id,
'name' => $intern->name,
'start_date' => $start->translatedFormat('d M Y'),
'end_date' => $end->translatedFormat('d M Y'),
'total_days' => $intern->internship_total_days,
'left' => $left,
'width' => $width,
'color' => $color,
'grid_start' => $gridStart,
'grid_end' => $gridEnd,
'hex_color' => $hexColor,
'is_stripes' => $isStripes,
'raw_start' => $intern->internship_start_date,
'raw_end' => $intern->internship_end_date,
];
});
return [
'interns' => $formattedInterns,
'months' => $months,
'todayPercent' => $todayPercent,
];
}
}
@@ -0,0 +1,218 @@
<?php
namespace App\Filament\Admin\Resources\JobApplications;
use App\Models\CareerApplication;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Builder;
class JobApplicationResource extends Resource
{
protected static ?string $model = CareerApplication::class;
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-briefcase';
public static function getNavigationLabel(): string
{
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
}
public static function getModelLabel(): string
{
return __('career.job', ['default' => 'İş Başvurusu']);
}
public static function getPluralModelLabel(): string
{
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->where('type', 'job');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label(__('career.name'))
->required()
->disabled(),
TextInput::make('email')
->label(__('career.email'))
->email()
->required()
->disabled(),
TextInput::make('phone')
->label(__('career.phone'))
->disabled(),
Select::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
])
->required(),
FileUpload::make('cv_path')
->label(__('career.cv'))
->disk('public')
->directory('cvs')
->required()
->disabled()
->downloadable(),
FileUpload::make('nda_path')
->label(__('career.nda'))
->disk('public')
->directory('ndas')
->disabled()
->downloadable(),
FileUpload::make('contract_path')
->label(__('career.contract'))
->disk('public')
->directory('contracts')
->disabled()
->downloadable(),
FileUpload::make('id_photocopy_path')
->label(__('career.id_photocopy', ['default' => 'Kimlik Fotokopisi']))
->disk('public')
->directory('id_photocopies')
->disabled()
->downloadable(),
Toggle::make('git_knowledge')
->label(__('career.git_knowledge'))
->disabled(),
Toggle::make('ai_usage')
->label(__('career.ai_usage'))
->disabled(),
Textarea::make('message')
->label(__('career.message'))
->disabled()
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('career.name'))
->searchable()
->sortable(),
TextColumn::make('email')
->label(__('career.email'))
->searchable()
->sortable(),
TextColumn::make('phone')
->label(__('career.phone'))
->searchable(),
TextColumn::make('status')
->label(__('career.status'))
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'gray',
'reviewed' => 'info',
'rejected' => 'danger',
'accepted' => 'success',
'waiting_document' => 'warning',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
TextColumn::make('git_knowledge')
->label(__('career.git_knowledge'))
->badge()
->color(fn ($state) => $state ? 'success' : 'danger')
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
TextColumn::make('ai_usage')
->label(__('career.ai_usage'))
->badge()
->color(fn ($state) => $state ? 'success' : 'danger')
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
TextColumn::make('created_at')
->label(__('career.created_at'))
->dateTime('d.m.Y H:i')
->sortable(),
])
->filters([
SelectFilter::make('status')
->label(__('career.status'))
->options([
'pending' => __('career.pending'),
'reviewed' => __('career.reviewed'),
'rejected' => __('career.rejected'),
'accepted' => __('career.accepted'),
'waiting_document' => __('career.waiting_document'),
]),
])
->actions([
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
Action::make('download_nda')
->label(__('career.nda'))
->icon('heroicon-o-shield-check')
->url(fn ($record) => $record->nda_path ? Storage::disk('public')->url($record->nda_path) : null)
->visible(fn ($record) => $record->nda_path !== null)
->openUrlInNewTab(),
Action::make('download_contract')
->label(__('career.contract'))
->icon('heroicon-o-document-text')
->url(fn ($record) => $record->contract_path ? Storage::disk('public')->url($record->contract_path) : null)
->visible(fn ($record) => $record->contract_path !== null)
->openUrlInNewTab(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListJobApplications::route('/'),
'create' => Pages\CreateJobApplication::route('/create'),
'edit' => Pages\EditJobApplication::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\JobApplications\Pages;
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateJobApplication extends CreateRecord
{
protected static string $resource = JobApplicationResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\JobApplications\Pages;
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditJobApplication extends EditRecord
{
protected static string $resource = JobApplicationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\JobApplications\Pages;
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
use Filament\Resources\Pages\ListRecords;
class ListJobApplications extends ListRecords
{
protected static string $resource = JobApplicationResource::class;
}
@@ -0,0 +1,73 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions;
use App\Filament\Admin\Resources\MusicProductions\Pages\CreateMusicProduction;
use App\Filament\Admin\Resources\MusicProductions\Pages\EditMusicProduction;
use App\Filament\Admin\Resources\MusicProductions\Pages\ListMusicProductions;
use App\Filament\Admin\Resources\MusicProductions\Schemas\MusicProductionForm;
use App\Filament\Admin\Resources\MusicProductions\Tables\MusicProductionsTable;
use App\Models\MusicProduction;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class MusicProductionResource extends Resource
{
protected static ?string $model = MusicProduction::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-musical-note';
public static function getNavigationLabel(): string
{
return __('music_productions.title');
}
public static function getModelLabel(): string
{
return __('music_productions.model_label');
}
public static function getPluralModelLabel(): string
{
return __('music_productions.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return MusicProductionForm::configure($schema);
}
public static function table(Table $table): Table
{
return MusicProductionsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListMusicProductions::route('/'),
'create' => CreateMusicProduction::route('/create'),
'edit' => EditMusicProduction::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Resources\Pages\CreateRecord;
class CreateMusicProduction extends CreateRecord
{
protected static string $resource = MusicProductionResource::class;
public function getTitle(): string
{
return __('music_productions.create');
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getCreatedNotificationTitle(): ?string
{
return __('music_productions.created_successfully');
}
protected function afterCreate(): void
{
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditMusicProduction extends EditRecord
{
protected static string $resource = MusicProductionResource::class;
public function getTitle(): string
{
return __('music_productions.edit');
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->label(__('music_productions.save'))
->action('save')
->keyBindings(['mod+s'])
->color('primary')
->size('sm'),
Action::make('cancel')
->label(__('music_productions.cancel'))
->url($this->getResource()::getUrl('index'))
->color('gray')
->size('sm'),
DeleteAction::make()
->label(__('music_productions.delete'))
->size('sm'),
RestoreAction::make()
->label(__('music_productions.restore'))
->size('sm'),
ForceDeleteAction::make()
->label(__('music_productions.force_delete'))
->size('sm'),
];
}
protected function getFormActions(): array
{
return []; // Hide standard bottom save/cancel buttons
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getSavedNotificationTitle(): ?string
{
return __('music_productions.updated_successfully');
}
protected function mutateFormDataBeforeFill(array $data): array
{
// Load existing translations
$translationData = TranslationTabs::fillFromRecord($this->record);
\Log::info('Loading translations for music production edit', [
'production_id' => $this->record->id,
'translations' => $translationData
]);
return array_merge($data, $translationData);
}
protected function afterSave(): void
{
\Log::info('Saving music production translations', [
'production_id' => $this->record->id,
'form_state' => $this->form->getState()
]);
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
use App\Console\Commands\SyncSpotifyMusicProductions;
use App\Console\Commands\SyncYoutubeReleases;
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
use App\Services\SpotifyService;
use App\Services\YouTubeService;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Facades\Artisan;
class ListMusicProductions extends ListRecords
{
protected static string $resource = MusicProductionResource::class;
public function getTitle(): string
{
return __('music_productions.title');
}
protected function getHeaderActions(): array
{
return [
Action::make('syncSpotify')
->label(__('music_productions.sync_spotify'))
->icon('heroicon-o-arrow-path')
->color('success')
->requiresConfirmation()
->modalHeading(__('music_productions.sync_spotify_heading'))
->modalDescription(__('music_productions.sync_spotify_description'))
->visible(fn (): bool => app(SpotifyService::class)->isConfigured())
->action(function (): void {
$exitCode = Artisan::call(SyncSpotifyMusicProductions::class);
if ($exitCode !== 0) {
Notification::make()
->title(__('music_productions.spotify_sync_failed'))
->danger()
->send();
return;
}
Notification::make()
->title(__('music_productions.spotify_sync_success'))
->success()
->send();
}),
Action::make('syncYoutube')
->label(__('music_productions.sync_youtube'))
->icon('heroicon-o-play')
->color('danger')
->requiresConfirmation()
->modalHeading(__('music_productions.sync_youtube_heading'))
->modalDescription(__('music_productions.sync_youtube_description'))
->visible(fn (): bool => app(YouTubeService::class)->isConfigured())
->action(function (): void {
$exitCode = Artisan::call(SyncYoutubeReleases::class);
if ($exitCode !== 0) {
Notification::make()
->title(__('music_productions.youtube_sync_failed'))
->danger()
->send();
return;
}
Notification::make()
->title(__('music_productions.youtube_sync_success'))
->success()
->send();
}),
CreateAction::make()
->label(__('music_productions.create')),
];
}
}
@@ -0,0 +1,199 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Models\MusicProduction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Placeholder;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Str;
class MusicProductionForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->columns(3)
->schema([
// Sol Kolon - Ana İçerik (2 sütun genişliğinde)
Section::make(__('music_productions.content_section'))
->schema([
TextInput::make('title')
->label(__('music_productions.title_field'))
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, callable $set) {
if ($operation !== 'create') {
return;
}
$set('slug', Str::slug($state));
}),
TextInput::make('slug')
->label(__('music_productions.slug_field'))
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->rules(['alpha_dash'])
->helperText(__('music_productions.slug_helper')),
TextInput::make('client_name')
->label(__('music_productions.client_name_field'))
->maxLength(255),
RichEditor::make('content')
->label(__('music_productions.content_field'))
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('music-productions')
->fileAttachmentsVisibility('public')
->columnSpanFull(),
])
->columnSpan(2)
->collapsible(false),
// Sağ Kolon - Ayarlar (1 sütun genişliğinde)
Section::make(__('music_productions.settings_section'))
->schema([
FileUpload::make('cover_image')
->label(__('music_productions.cover_image_field'))
->image()
->disk('public')
->directory('music-productions/covers')
->visibility('public')
->imageEditor()
->imageEditorAspectRatios([
'16:9',
'4:3',
'1:1',
])
->helperText(__('music_productions.cover_image_helper'))
->columnSpanFull(),
DatePicker::make('production_date')
->label(__('music_productions.production_date_field'))
->displayFormat('d.m.Y')
->helperText(__('music_productions.production_date_helper'))
->columnSpanFull(),
TextInput::make('sort_order')
->label(__('music_productions.sort_order_field'))
->numeric()
->default(0)
->columnSpanFull(),
Checkbox::make('is_active')
->label(__('music_productions.is_active_field'))
->default(true)
->helperText(__('music_productions.is_active_helper'))
->columnSpanFull(),
])
->columnSpan(1)
->collapsible(false),
Section::make(__('music_productions.spotify_section'))
->schema([
Placeholder::make('spotify_album_id_display')
->label(__('music_productions.spotify_album_id_field'))
->content(fn (?MusicProduction $record): string => $record?->spotify_album_id ?? '-'),
Placeholder::make('spotify_type_display')
->label(__('music_productions.spotify_type_field'))
->content(fn (?MusicProduction $record): string => $record?->spotify_type
? __('music_productions.spotify_type_' . $record->spotify_type, [], $record->spotify_type)
: '-'),
Placeholder::make('spotify_url_display')
->label(__('music_productions.spotify_url_field'))
->content(fn (?MusicProduction $record): string => $record?->spotify_url ?? '-'),
Placeholder::make('spotify_synced_at_display')
->label(__('music_productions.spotify_synced_at_field'))
->content(fn (?MusicProduction $record): string => $record?->spotify_synced_at
?->timezone(config('app.timezone'))
->format('d.m.Y H:i') ?? '-'),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true)
->visible(fn (?MusicProduction $record): bool => filled($record?->spotify_album_id)),
Section::make(__('music_productions.youtube_section'))
->schema([
Placeholder::make('youtube_video_id_display')
->label(__('music_productions.youtube_video_id_field'))
->content(fn (?MusicProduction $record): string => $record?->youtube_video_id ?? '-'),
Placeholder::make('youtube_url_display')
->label(__('music_productions.youtube_url_field'))
->content(fn (?MusicProduction $record): string => $record?->youtube_url ?? '-'),
Placeholder::make('youtube_description_display')
->label(__('music_productions.youtube_description_field'))
->content(fn (?MusicProduction $record): string => filled($record?->youtube_description)
? \Illuminate\Support\Str::limit($record->youtube_description, 500)
: '-'),
Placeholder::make('youtube_synced_at_display')
->label(__('music_productions.youtube_synced_at_field'))
->content(fn (?MusicProduction $record): string => $record?->youtube_synced_at
?->timezone(config('app.timezone'))
->format('d.m.Y H:i') ?? '-'),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true)
->visible(fn (?MusicProduction $record): bool => filled($record?->youtube_video_id)),
// Alt Kısım - Galeri Görselleri
Section::make(__('music_productions.gallery_field'))
->schema([
FileUpload::make('gallery')
->label(__('music_productions.gallery_field'))
->multiple()
->image()
->disk('public')
->directory('music-productions/gallery')
->visibility('public')
->helperText(__('music_productions.gallery_helper'))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false),
// Çeviriler Kısımı (Çoklu Dil)
Section::make('🌍 ' . __('music_productions.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('music_productions.title_field'),
'required' => false,
'maxLength' => 255,
],
'client_name' => [
'type' => 'text',
'label' => __('music_productions.client_name_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'richtext',
'label' => __('music_productions.content_field'),
'required' => false,
],
]),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false),
]);
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Filament\Admin\Resources\MusicProductions\Tables;
use App\Models\MusicProduction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
class MusicProductionsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
ImageColumn::make('cover_image')
->label(__('music_productions.cover_image_field'))
->getStateUsing(fn (MusicProduction $record): ?string => $record->cover_image_url)
->square()
->size(60),
TextColumn::make('title')
->label(__('music_productions.table_title'))
->searchable()
->sortable()
->limit(50),
TextColumn::make('slug')
->label(__('music_productions.table_slug'))
->searchable()
->sortable()
->limit(30),
TextColumn::make('client_name')
->label(__('music_productions.table_client_name'))
->searchable()
->sortable()
->limit(30),
TextColumn::make('production_date')
->label(__('music_productions.table_production_date'))
->date('d.m.Y')
->sortable(),
TextColumn::make('spotify_type')
->label(__('music_productions.spotify_type_field'))
->badge()
->formatStateUsing(fn (?string $state): string => filled($state)
? __('music_productions.spotify_type_' . $state, [], $state)
: '-')
->color(fn (?string $state): string => match ($state) {
'single' => 'info',
'album' => 'success',
default => 'gray',
})
->toggleable(),
ToggleColumn::make('is_active')
->label(__('music_productions.table_is_active'))
->alignCenter(),
TextColumn::make('sort_order')
->label(__('music_productions.sort_order_field'))
->sortable()
->alignCenter(),
TextColumn::make('created_at')
->label(__('music_productions.table_created_at'))
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label(__('music_productions.table_updated_at'))
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
TernaryFilter::make('is_active')
->label(__('music_productions.is_active_field')),
])
->recordActions([
EditAction::make()
->label(__('music_productions.edit')),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->label(__('music_productions.delete')),
RestoreBulkAction::make()
->label(__('music_productions.restore')),
ForceDeleteBulkAction::make()
->label(__('music_productions.force_delete')),
]),
])
->defaultSort('sort_order', 'asc');
}
}
@@ -51,7 +51,7 @@ class PageResource extends Resource
public static function getRelations(): array
{
return [
//
RelationManagers\TestimonialsRelationManager::class,
];
}
@@ -0,0 +1,122 @@
<?php
namespace App\Filament\Admin\Resources\Pages\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class TestimonialsRelationManager extends RelationManager
{
protected static string $relationship = 'testimonials';
protected static ?string $title = 'Müşteri Görüşleri';
protected static ?string $modelLabel = 'Müşteri Görüşü';
protected static ?string $pluralModelLabel = 'Müşteri Görüşleri';
protected static ?string $recordTitleAttribute = 'name';
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
{
// Sadece Müşteri Görüşleri sayfası veya testimonials şablonu kullananlar için göster
return $ownerRecord->slug === 'musteri-gorusleri' ||
$ownerRecord->template === 'corporate.testimonials' ||
$ownerRecord->is_homepage;
}
public function form(Schema $schema): Schema
{
return $schema
->schema([
TextInput::make('name')
->label('Müşteri Adı')
->required()
->maxLength(255),
TextInput::make('position')
->label('Şirket / Ünvan')
->maxLength(255),
Textarea::make('content')
->label('Görüş / Yorum')
->required()
->columnSpanFull(),
Select::make('rating')
->label('Puan')
->options([
1 => '1 Yıldız',
2 => '2 Yıldız',
3 => '3 Yıldız',
4 => '4 Yıldız',
5 => '5 Yıldız',
])
->default(5)
->required(),
FileUpload::make('avatar')
->label('Profil Resmi')
->image()
->directory('testimonials'),
Toggle::make('is_active')
->label('Aktif')
->default(true),
TextInput::make('sort_order')
->label('Sıralama')
->numeric()
->default(0),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name')
->label('Ad')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('position')
->label('Ünvan')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('rating')
->label('Puan')
->sortable(),
Tables\Columns\IconColumn::make('is_active')
->label('Aktif')
->boolean()
->sortable(),
Tables\Columns\TextColumn::make('sort_order')
->label('Sıra')
->sortable(),
])
->filters([
//
])
->headerActions([
CreateAction::make(),
])
->actions([
EditAction::make(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -9,6 +9,9 @@ use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\LogosSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PartnersSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\BankAccountsSection;
use Filament\Schemas\Schema;
@@ -49,6 +52,11 @@ class PageForm
SectionTemplatesSection::make(),
FooterTemplateSection::make(),
// Full Width - Logos (Only for Logolarımız template)
LogosSection::make(),
PartnersSection::make(),
BankAccountsSection::make(),
// Full Width - Translations
TranslationsSection::make(),
]);
@@ -0,0 +1,75 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class BankAccountsSection
{
public static function make(): Section
{
return Section::make(__('Banka Hesapları'))
->description(__('Bu sayfada gösterilecek banka hesap bilgilerini buradan yönetebilirsiniz.'))
->schema([
Repeater::make('data.bank_accounts')
->label('Hesaplar')
->schema([
TextInput::make('bank_name')
->label('Banka Adı')
->placeholder('Örn: Ziraat Bankası')
->required(),
TextInput::make('branch_name')
->label('Şube Adı / Kodu')
->placeholder('Örn: Konak Şubesi / 1234'),
TextInput::make('account_holder')
->label('Hesap Sahibi')
->default('Trunçgil Teknoloji')
->required(),
TextInput::make('iban')
->label('IBAN')
->placeholder('TR00 0000 0000 0000 0000 0000 00')
->required(),
Select::make('currency')
->label('Para Birimi')
->options([
'TRY' => 'Türk Lirası (₺)',
'USD' => 'Amerikan Doları ($)',
'EUR' => 'Euro (€)',
'GBP' => 'Sterlin (£)',
])
->default('TRY')
->live()
->required(),
TextInput::make('swift_code')
->label('SWIFT / BIC')
->placeholder('Örn: TRHBTR2A')
->required(fn (Get $get): bool => $get('currency') !== 'TRY'),
FileUpload::make('logo')
->label('Banka Logosu')
->image()
->disk('public')
->directory('banks')
->visibility('public'),
])
->grid(3)
->columnSpanFull()
->reorderableWithButtons()
->collapsible()
->cloneable(),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.bank-accounts')
->columnSpanFull()
->collapsible();
}
}
@@ -35,7 +35,6 @@ class ContentSection
RichEditor::make('content')
->label(__('pages.content_field'))
->required()
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('pages')
->fileAttachmentsVisibility('public')
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Facades\File;
class FooterTemplateSection
{
public static function make(): Section
@@ -17,9 +19,35 @@ class FooterTemplateSection
return Section::make(__('pages.form_section_footer_template'))
->description(__('pages.form_section_footer_template_desc'))
->schema([
Select::make('custom_footer_blade')
->label('Custom Footer Blade')
->options(function () {
$files = File::glob(resource_path('views/partials/*.blade.php'));
$options = [];
foreach ($files as $file) {
$name = basename($file, '.blade.php');
$options["partials.{$name}"] = "Partial: {$name}";
}
// Add other directories if needed
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
foreach ($componentFiles as $file) {
$name = basename($file, '.blade.php');
$options["components.front.{$name}"] = "Component: {$name}";
}
return $options;
})
->searchable()
->preload()
->helperText('Select a physical Blade file to use as footer instead of a database template.')
->live()
->columnSpanFull(),
Select::make('footer_template_id')
->label(__('pages.footer_template_field'))
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => empty($get('custom_footer_blade')))
->searchable()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Facades\File;
class HeaderTemplateSection
{
public static function make(): Section
@@ -17,9 +19,35 @@ class HeaderTemplateSection
return Section::make(__('pages.form_section_header_template'))
->description(__('pages.form_section_header_template_desc'))
->schema([
Select::make('custom_header_blade')
->label('Custom Header Blade')
->options(function () {
$files = File::glob(resource_path('views/partials/*.blade.php'));
$options = [];
foreach ($files as $file) {
$name = basename($file, '.blade.php');
$options["partials.{$name}"] = "Partial: {$name}";
}
// Add other directories if needed
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
foreach ($componentFiles as $file) {
$name = basename($file, '.blade.php');
$options["components.front.{$name}"] = "Component: {$name}";
}
return $options;
})
->searchable()
->preload()
->helperText('Select a physical Blade file to use as header instead of a database template.')
->live()
->columnSpanFull(),
Select::make('header_template_id')
->label(__('pages.header_template_field'))
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => empty($get('custom_header_blade')))
->searchable()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
@@ -0,0 +1,36 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class LogosSection
{
public static function make(): Section
{
return Section::make(__('Logolar'))
->description(__('Bu sayfada gösterilecek logoları buradan yönetebilirsiniz.'))
->schema([
Grid::make(3)
->schema([
FileUpload::make('data.logo_1')
->label(__('Logo 1'))
->disk('public'),
FileUpload::make('data.logo_2')
->label(__('Logo 2'))
->disk('public'),
FileUpload::make('data.logo_3')
->label(__('Logo 3'))
->disk('public'),
]),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.logos')
->columnSpanFull()
->collapsible();
}
}
@@ -2,12 +2,14 @@
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use App\Support\PageTemplateHero;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class PageSettingsSection
{
@@ -15,6 +17,13 @@ class PageSettingsSection
{
return Section::make(__('pages.form_section_page_settings'))
->schema([
Select::make('template')
->label(__('pages.template_field'))
->options(PageTemplateHero::templateFormOptions())
->default('default')
->live()
->columnSpanFull(),
FileUpload::make('featured_image')
->label(__('pages.featured_image_field'))
->image()
@@ -27,7 +36,13 @@ class PageSettingsSection
'4:3',
'1:1',
])
->helperText(__('pages.featured_image_helper_text'))
->helperText(function (Get $get): string {
if (PageTemplateHero::hasHero($get('template'))) {
return __('pages.featured_image_template_hero_helper');
}
return __('pages.featured_image_helper_text');
})
->columnSpanFull(),
Select::make('author_id')
@@ -61,21 +76,10 @@ class PageSettingsSection
->preload()
->helperText(__('pages.parent_helper_text'))
->columnSpanFull(),
Select::make('template')
->label(__('pages.template_field'))
->options([
'default' => __('pages.template_default'),
'landing' => __('pages.template_landing'),
'blog' => __('pages.template_blog'),
'contact' => __('pages.template_contact'),
])
->default('default')
->columnSpanFull(),
TextInput::make('sort_order')
->label(__('pages.sort_order_field'))
->numeric()
->label(__('pages.sort_order_field'))
->numeric()
->default(0)
->helperText(__('pages.sort_order_helper_text'))
->columnSpanFull(),
@@ -0,0 +1,53 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class PartnersSection
{
public static function make(): Section
{
return Section::make(__('Çözüm Ortakları'))
->description(__('Bu sayfada gösterilecek çözüm ortaklarını buradan yönetebilirsiniz.'))
->schema([
Repeater::make('data.partners')
->label('Partnerler')
->schema([
TextInput::make('name')
->label('Partner Adı')
->placeholder('Örn: Microsoft')
->maxLength(120)
->columnSpanFull(),
FileUpload::make('logo')
->label('Logo')
->image()
->disk('public')
->directory('partners')
->visibility('public')
->required()
->columnSpanFull(),
TextInput::make('url')
->label('Website URL')
->url()
->placeholder('https://example.com')
->columnSpanFull(),
])
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
->grid(4)
->columnSpanFull()
->reorderableWithButtons()
->collapsible()
->cloneable(),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.partners')
->columnSpanFull()
->collapsible();
}
}
@@ -233,7 +233,7 @@ class SectionBuilderSection
->image()
->disk('public')
->directory('pages/sections')
->imageEditor()
->directory('pages/sections')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProductCategory extends CreateRecord
{
protected static string $resource = ProductCategoryResource::class;
protected function afterCreate(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditProductCategory extends EditRecord
{
protected static string $resource = ProductCategoryResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function afterSave(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProductCategories extends ListRecords
{
protected static string $resource = ProductCategoryResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories;
use App\Filament\Admin\Resources\ProductCategories\Pages\CreateProductCategory;
use App\Filament\Admin\Resources\ProductCategories\Pages\EditProductCategory;
use App\Filament\Admin\Resources\ProductCategories\Pages\ListProductCategories;
use App\Filament\Admin\Resources\ProductCategories\Schemas\ProductCategoryForm;
use App\Filament\Admin\Resources\ProductCategories\Tables\ProductCategoriesTable;
use App\Models\ProductCategory;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class ProductCategoryResource extends Resource
{
protected static ?string $model = ProductCategory::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
public static function getNavigationGroup(): ?string
{
return __('product_categories.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('product_categories.menu_label');
}
public static function getModelLabel(): string
{
return __('product_categories.model_label');
}
public static function getPluralModelLabel(): string
{
return __('product_categories.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return ProductCategoryForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProductCategoriesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProductCategories::route('/'),
'create' => CreateProductCategory::route('/create'),
'edit' => EditProductCategory::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Schemas;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Models\ProductCategory;
class ProductCategoryForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Section::make(__('product_categories.general_section'))
->schema([
TextInput::make('slug')
->label(__('product_categories.slug'))
->required()
->unique(ignoreRecord: true),
Select::make('parent_id')
->label(__('product_categories.parent_category'))
->options(function () {
return ProductCategory::all()->mapWithKeys(function ($category) {
$title = is_array($category->title)
? ($category->title[app()->getLocale()] ?? \Illuminate\Support\Arr::first($category->title))
: $category->title;
return [$category->id => $title ?? ('Category #' . $category->id)];
});
})
->searchable(),
TextInput::make('sort_order')
->label(__('product_categories.sort_order'))
->numeric()
->default(0),
Toggle::make('is_active')
->label(__('product_categories.is_active'))
->default(true),
])->columns(2),
Section::make(__('product_categories.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('product_categories.title'),
'required' => true,
],
]),
]),
]);
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\BulkActionGroup;
class ProductCategoriesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label(__('product_categories.title'))
->getStateUsing(fn ($record) => $record->translate('title'))
->searchable(query: function ($query, $search) {
return $query->whereHas('translations', function ($q) use ($search) {
$q->where('field_name', 'title')
->where('field_value', 'like', "%{$search}%")
->where('language_code', app()->getLocale());
});
}),
TextColumn::make('slug')
->label(__('product_categories.slug'))
->searchable(),
TextColumn::make('parent.title')
->getStateUsing(fn ($record) => $record->parent?->translate('title'))
->label(__('product_categories.parent')),
TextColumn::make('sort_order')
->label(__('product_categories.sort_order'))
->sortable(),
ToggleColumn::make('is_active')
->label(__('product_categories.is_active')),
TextColumn::make('created_at')
->label(__('product_categories.created_at'))
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProduct extends CreateRecord
{
protected static string $resource = ProductResource::class;
protected function afterCreate(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditProduct extends EditRecord
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function afterSave(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProducts extends ListRecords
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\Products;
use App\Filament\Admin\Resources\Products\Pages\CreateProduct;
use App\Filament\Admin\Resources\Products\Pages\EditProduct;
use App\Filament\Admin\Resources\Products\Pages\ListProducts;
use App\Filament\Admin\Resources\Products\Schemas\ProductForm;
use App\Filament\Admin\Resources\Products\Tables\ProductsTable;
use App\Models\Product;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class ProductResource extends Resource
{
protected static ?string $model = Product::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
public static function getNavigationGroup(): ?string
{
return __('products.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('products.menu_label');
}
public static function getModelLabel(): string
{
return __('products.model_label');
}
public static function getPluralModelLabel(): string
{
return __('products.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return ProductForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProductsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProducts::route('/'),
'create' => CreateProduct::route('/create'),
'edit' => EditProduct::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,218 @@
<?php
namespace App\Filament\Admin\Resources\Products\Schemas;
use App\Models\Language;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
class LandingPageSection
{
private static function getIconOptions(): array
{
$icons = [
'cloud-network-2', 'touchscreen', 'lock', 'rocket', 'bar-chart', 'safe',
'controls', 'checked', 'calendar', 'currency', 'server', 'devices',
'search', 'compare', 'smartphone', 'headphone', 'secure', 'verified',
'wallet', 'shopping-cart', 'e-commerce', 'code', 'bulb', 'gears',
'shield', 'star', 'check-circle', 'bolt', 'zap', 'heart', 'gift',
];
return array_combine($icons, array_map(fn($icon) => ucwords(str_replace('-', ' ', $icon)), $icons));
}
private static function getBackgroundOptions(): array
{
$backgrounds = [
// Abstract backgrounds
'assets/img/photos/bg2.jpg' => 'Background 2 (Abstract)',
'assets/img/photos/bg3.jpg' => 'Background 3 (Abstract)',
'assets/img/photos/bg4.jpg' => 'Background 4 (Abstract)',
'assets/img/photos/bg13.jpg' => 'Background 13 (Abstract)',
// Gradient backgrounds
'assets/img/photos/bg14.png' => 'Background 14 (Gradient)',
'assets/img/photos/bg15.png' => 'Background 15 (Gradient)',
'assets/img/photos/bg16.png' => 'Background 16 (Gradient)',
'assets/img/photos/bg17.png' => 'Background 17 (Gradient)',
'assets/img/photos/bg18.png' => 'Background 18 (Gradient)',
'assets/img/photos/bg19.png' => 'Background 19 (Gradient)',
'assets/img/photos/bg20.png' => 'Background 20 (Gradient)',
'assets/img/photos/bg21.png' => 'Background 21 (Gradient)',
'assets/img/photos/bg22.png' => 'Background 22 (Gradient)',
'assets/img/photos/bg23.png' => 'Background 23 (Gradient)',
'assets/img/photos/bg24.png' => 'Background 24 (Gradient)',
'assets/img/photos/bg25.png' => 'Background 25 (Gradient)',
];
return $backgrounds;
}
public static function make(): Section
{
$languages = Language::active()->ordered()->get();
$iconOptions = self::getIconOptions();
$backgroundOptions = self::getBackgroundOptions();
return Section::make(__('products.landing_page_section'))
->schema([
// Hero Section
Section::make(__('products.hero_section'))
->schema([
TextInput::make('landing_page_data.hero.slogan')
->label(__('products.hero_slogan'))
->columnSpanFull(),
Textarea::make('landing_page_data.hero.sub_slogan')
->label(__('products.hero_sub_slogan'))
->rows(2)
->columnSpanFull(),
Select::make('landing_page_data.hero.background_image')
->label(__('products.hero_background_image'))
->options($backgroundOptions)
->searchable()
->placeholder(__('products.select_background'))
->helperText(__('products.hero_background_image_helper'))
->columnSpanFull(),
TextInput::make('landing_page_data.hero.app_store_link')
->label(__('products.app_store_link'))
->url()
->placeholder('https://apps.apple.com/...'),
TextInput::make('landing_page_data.hero.google_play_link')
->label(__('products.google_play_link'))
->url()
->placeholder('https://play.google.com/...'),
FileUpload::make('landing_page_data.hero.featured_image')
->label(__('products.hero_featured_image'))
->image()
->disk('public')
->directory('products/landing')
->columnSpanFull(),
])
->columns(2)
->collapsible()
->collapsed(),
// App Features Section
Section::make(__('products.app_features_section'))
->schema([
Repeater::make('landing_page_data.features')
->label(__('products.features'))
->schema([
TextInput::make('icon')
->label(__('products.feature_icon')),
TextInput::make('title')
->label(__('products.feature_title'))
->columnSpanFull(),
Textarea::make('description')
->label(__('products.feature_description'))
->rows(2)
->columnSpanFull(),
])
->columns(1)
->defaultItems(0)
->addActionLabel(__('products.add_feature'))
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => $state['title'] ?? __('products.feature') . ' #' . ($state['_index'] ?? ''))
->columnSpanFull(),
// Note: Translations should match the order of features above
// Each feature translation array index should correspond to the feature index
])
->collapsible()
->collapsed(),
// How It Works Section
Section::make(__('products.how_it_works_section'))
->schema([
FileUpload::make('landing_page_data.how_it_works.download_image')
->label(__('products.how_it_works_image'))
->image()
->disk('public')
->directory('products/landing')
->columnSpanFull(),
TextInput::make('landing_page_data.how_it_works.download_form_label')
->label(__('products.download_form_label'))
->placeholder(__('products.download_form_label_placeholder'))
->columnSpanFull(),
// Steps (4 steps with translations)
Repeater::make('landing_page_data.how_it_works.steps')
->label(__('products.steps'))
->schema([
TextInput::make('number')
->label(__('products.step_number'))
->numeric()
->default(fn ($get) => ($get('../../../_index') ?? 0) + 1),
TextInput::make('title')
->label(__('products.step_title'))
->columnSpanFull(),
Textarea::make('description')
->label(__('products.step_description'))
->rows(2)
->columnSpanFull(),
])
->columns(1)
->defaultItems(0)
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => __('products.step') . ' ' . ($state['number'] ?? '') . ': ' . ($state['title'] ?? ''))
->columnSpanFull(),
// Note: Steps translations should match the order of steps above
// Each step translation array index should correspond to the step index
])
->collapsible()
->collapsed(),
// Video Section
Section::make(__('products.video_section'))
->schema([
TextInput::make('landing_page_data.video.youtube_video_id')
->label(__('products.youtube_video_id'))
->placeholder('165101721')
->helperText(__('products.youtube_video_id_helper'))
->columnSpanFull(),
])
->collapsible()
->collapsed(),
// FAQ Section
Section::make(__('products.faq_section'))
->schema([
Repeater::make('landing_page_data.faqs')
->label(__('products.faqs'))
->schema([
TextInput::make('question')
->label(__('products.faq_question'))
->columnSpanFull(),
Textarea::make('answer')
->label(__('products.faq_answer'))
->rows(3)
->columnSpanFull(),
])
->columns(1)
->defaultItems(0)
->addActionLabel(__('products.add_faq'))
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => $state['question'] ?? __('products.faq') . ' #' . ($state['_index'] ?? ''))
->columnSpanFull(),
// Note: FAQ translations should match the order of FAQs above
// Each FAQ translation array index should correspond to the FAQ index
])
->collapsible()
->collapsed(),
])
->visible(fn (Get $get) => $get('type') === 'product')
->columnSpanFull();
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Filament\Admin\Resources\Products\Schemas;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Section;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\Schemas\LandingPageSection;
use App\Models\ProductCategory;
use Illuminate\Support\Str;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class ProductForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Section::make(__('products.general_section'))
->schema([
TextInput::make('title')
->label(__('products.title'))
->live(onBlur: true)
->afterStateUpdated(function (Get $get, Set $set, ?string $state) {
$set('slug', Str::slug($state));
$set('translations.' . app()->getLocale() . '.title', $state);
})
->formatStateUsing(fn ($record) => $record?->translate('title'))
->dehydrated(false),
TextInput::make('slug')
->label(__('products.slug'))
->unique(ignoreRecord: true),
Select::make('type')
->label(__('products.type'))
->options([
'product' => __('products.product'),
'service' => __('products.service'),
]),
Select::make('product_category_id')
->label(__('products.category'))
->options(function () {
return ProductCategory::all()->mapWithKeys(function ($category) {
return [$category->id => $category->translate('title') ?? ('Category #' . $category->id)];
});
})
->searchable(),
FileUpload::make('hero_image')
->label(__('products.hero_image'))
->image()
->disk('public')
->directory('products'),
TextInput::make('view_template')
->label(__('products.view_template'))
->placeholder(__('products.view_template_placeholder'))
->helperText(__('products.view_template_helper')),
TextInput::make('external_url')
->label(__('products.external_url'))
->url()
->helperText(__('products.external_url_helper')),
TextInput::make('sort_order')
->label(__('products.sort_order'))
->numeric()
->default(0),
Toggle::make('is_active')
->label(__('products.is_active'))
->default(true),
])->columns(2),
Section::make(__('products.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('products.title'),
'required' => false,
],
'content' => [
'type' => 'richtext',
'label' => __('products.content'),
],
]),
]),
// Landing Page Section (only for products)
LandingPageSection::make(),
]);
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\Products\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\BulkActionGroup;
class ProductsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
ImageColumn::make('hero_image')
->label(__('products.hero_image'))
->disk('public'),
TextColumn::make('title')
->label(__('products.title'))
->getStateUsing(fn ($record) => $record->translate('title'))
->searchable(query: function ($query, $search) {
return $query->whereHas('translations', function ($q) use ($search) {
$q->where('field_name', 'title')
->where('field_value', 'like', "%{$search}%")
->where('language_code', app()->getLocale());
});
}),
TextColumn::make('type')
->label(__('products.type'))
->badge()
->color(fn (string $state): string => match ($state) {
'product' => 'info',
'service' => 'success',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => match ($state) {
'product' => __('products.product'),
'service' => __('products.service'),
default => $state,
}),
TextColumn::make('category.title')
->getStateUsing(fn ($record) => $record->category?->translate('title'))
->label(__('products.category')),
ToggleColumn::make('is_active')
->label(__('products.is_active')),
TextColumn::make('sort_order')
->label(__('products.sort_order'))
->sortable(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Admin\Resources\Proposals\Pages;
use App\Filament\Admin\Resources\Proposals\ProposalResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreateProposal extends CreateRecord
{
protected static string $resource = ProposalResource::class;
public function getTitle(): string
{
return __('proposal.create');
}
public function getMaxContentWidth(): Width | string | null
{
return Width::Full;
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Filament\Admin\Resources\Proposals\Pages;
use App\Filament\Admin\Resources\Proposals\ProposalResource;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
use Illuminate\Support\Str;
class EditProposal extends EditRecord
{
protected static string $resource = ProposalResource::class;
public function getTitle(): string
{
return __('proposal.edit');
}
public function getMaxContentWidth(): Width | string | null
{
return Width::Full;
}
protected function getHeaderActions(): array
{
return [
// Teklif linkini tarayıcı panosuna kopyala
Action::make('copy_link')
->label(__('proposal.copy_link'))
->icon('heroicon-o-clipboard-document-check')
->color('info')
->action(function () {
$url = route('proposals.show', $this->record->slug);
// Livewire v3: sunucu tarafında JS enjekte et (user-gesture gerektirmez)
$escaped = addslashes($url);
$this->js("(function(){var t=document.createElement('textarea');t.value='{$escaped}';t.style.position='fixed';t.style.opacity='0';document.body.appendChild(t);t.focus();t.select();try{document.execCommand('copy')}catch(e){}document.body.removeChild(t);})();");
Notification::make()
->title(__('proposal.link_copied'))
->body($url)
->success()
->duration(6000)
->send();
})
->visible(fn () => $this->record !== null),
// Yeni rastgele benzersiz slug üret ve kaydet
Action::make('regenerate_slug')
->label(__('proposal.regenerate_slug'))
->icon('heroicon-o-arrow-path')
->color('warning')
->requiresConfirmation()
->modalHeading(__('proposal.regenerate_slug_confirm_heading'))
->modalDescription(__('proposal.regenerate_slug_confirm_body'))
->modalSubmitActionLabel(__('proposal.regenerate_slug_confirm_button'))
->action(function () {
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
// Benzersizliği garanti et
while (\App\Models\Proposal::where('slug', $newSlug)->where('id', '!=', $this->record->id)->exists()) {
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
}
$this->record->update(['slug' => $newSlug]);
$newUrl = route('proposals.show', $newSlug);
Notification::make()
->title(__('proposal.regenerate_slug_success'))
->body($newUrl)
->success()
->duration(8000)
->send();
// Formu yenile
$this->fillForm();
})
->visible(fn () => $this->record !== null),
// Teklifi yeni sekmede önizle
Action::make('preview')
->label(__('proposal.preview'))
->icon('heroicon-o-eye')
->color('gray')
->url(fn () => $this->record ? route('proposals.show', $this->record->slug) : '#')
->openUrlInNewTab()
->visible(fn () => $this->record !== null),
];
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Admin\Resources\Proposals\Pages;
use App\Filament\Admin\Resources\Proposals\ProposalResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProposals extends ListRecords
{
protected static string $resource = ProposalResource::class;
public function getTitle(): string
{
return __('proposal.title');
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label(__('proposal.create')),
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Filament\Admin\Resources\Proposals;
use App\Filament\Admin\Resources\Proposals\Pages\CreateProposal;
use App\Filament\Admin\Resources\Proposals\Pages\EditProposal;
use App\Filament\Admin\Resources\Proposals\Pages\ListProposals;
use App\Filament\Admin\Resources\Proposals\Schemas\ProposalForm;
use App\Filament\Admin\Resources\Proposals\Tables\ProposalsTable;
use App\Models\Proposal;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class ProposalResource extends Resource
{
protected static ?string $model = Proposal::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
public static function getNavigationLabel(): string
{
return __('proposal.navigation_label');
}
public static function getModelLabel(): string
{
return __('proposal.model_label');
}
public static function getPluralModelLabel(): string
{
return __('proposal.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return ProposalForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProposalsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProposals::route('/'),
'create' => CreateProposal::route('/create'),
'edit' => EditProposal::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,164 @@
<?php
namespace App\Filament\Admin\Resources\Proposals\Schemas;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\CodeEditor\Enums\Language as CodeEditorLanguage;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
class ProposalForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Tabs::make('Tabs')
->tabs([
Tab::make('Teklif Detayları ve İçerik')
->icon('heroicon-m-document-text')
->schema([
// Section 2: side-by-side Markdown editor (CodeMirror) + live preview
Section::make(__('proposal.content_section'))
->description('Sol tarafta Markdown formatında içeriği düzenleyin, sağ tarafta canlı olarak nasıl görüneceğini izleyin.')
->columns(2)
->schema([
CodeEditor::make('content')
->label(__('proposal.content_field'))
->hiddenLabel()
->required()
->language(CodeEditorLanguage::Markdown)
->live(onBlur: false)
->columnSpan(1)
->extraAttributes([
'style' => 'min-height: 700px; max-height: 700px; overflow-y: auto;',
'data-field-name' => 'content',
]),
View::make('filament.components.markdown-preview')
->columnSpan(1),
]),
]),
Tab::make('Durum ve Ayarlar')
->icon('heroicon-m-cog-6-tooth')
->schema([
// Section 1: compact top metadata and configuration
Section::make(__('proposal.settings_section'))
->columns(12)
->schema([
TextInput::make('title')
->label(__('proposal.title_field'))
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, callable $set) {
if ($operation !== 'create') {
return;
}
$set('slug', \Str::slug($state));
})
->columnSpan(4),
TextInput::make('slug')
->label(__('proposal.slug_field'))
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->rules(['alpha_dash'])
->helperText(__('proposal.slug_helper'))
->columnSpan(4),
Select::make('status')
->label(__('proposal.status_field'))
->options([
'draft' => __('proposal.status_draft'),
'sent' => __('proposal.status_sent'),
'accepted' => __('proposal.status_accepted'),
'rejected' => __('proposal.status_rejected'),
'revised' => __('proposal.status_revised'),
])
->default('draft')
->required()
->columnSpan(4),
TextInput::make('client_name')
->label(__('proposal.client_name_field'))
->required()
->maxLength(255)
->columnSpan(4),
TextInput::make('client_email')
->label(__('proposal.client_email_field'))
->email()
->maxLength(255)
->columnSpan(4),
DatePicker::make('valid_until')
->label(__('proposal.valid_until_field'))
->native(false)
->displayFormat('d.m.Y')
->columnSpan(4),
TextInput::make('total_price')
->label(__('proposal.total_price_field'))
->numeric()
->prefix(fn ($get) => match ($get('currency')) {
'USD' => '$',
'EUR' => '€',
default => '₺',
})
->columnSpan(3),
Select::make('currency')
->label(__('proposal.currency_field'))
->options([
'TRY' => 'TL (₺)',
'USD' => 'USD ($)',
'EUR' => 'EUR (€)',
])
->default('TRY')
->required()
->live()
->columnSpan(1),
Select::make('meta.accent_color')
->label(__('proposal.accent_color_field'))
->options([
'indigo' => 'Premium Indigo (Mor/Mavi)',
'emerald' => 'Emerald Medical (Zümrüt Yeşil)',
'cyberpunk' => 'Cyberpunk Neon (Pembe/Mavi)',
'coral' => 'Coral Corporate (Mercan Kırmızı)',
'amber' => 'Amber Executive (Kehribar/Altın)',
])
->default('indigo')
->live()
->columnSpan(4),
Checkbox::make('meta.show_calculator')
->label(__('proposal.show_calculator_field'))
->default(true)
->columnSpan(4),
// Client feedback inside this section — only visible when present
Textarea::make('client_feedback')
->label('Müşteri Geri Bildirimi / Revizyon Notu')
->rows(3)
->disabled()
->dehydrated(false)
->placeholder('Henüz geri bildirim bırakılmadı.')
->visible(fn ($record) => $record !== null && !empty($record->client_feedback))
->columnSpanFull(),
]),
]),
])->columnSpanFull()
]);
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Filament\Admin\Resources\Proposals\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class ProposalsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label(__('proposal.table_title'))
->searchable()
->sortable()
->limit(40),
TextColumn::make('client_name')
->label(__('proposal.table_client'))
->searchable()
->sortable()
->limit(30),
TextColumn::make('total_price')
->label(__('proposal.table_price'))
->money(fn ($record) => $record->currency)
->sortable(),
TextColumn::make('status')
->label(__('proposal.table_status'))
->badge()
->color(fn (string $state): string => match ($state) {
'draft' => 'gray',
'sent' => 'info',
'accepted' => 'success',
'rejected' => 'danger',
'revised' => 'warning',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => match ($state) {
'draft' => __('proposal.status_draft'),
'sent' => __('proposal.status_sent'),
'accepted' => __('proposal.status_accepted'),
'rejected' => __('proposal.status_rejected'),
'revised' => __('proposal.status_revised'),
default => $state,
}),
TextColumn::make('slug')
->label(__('proposal.table_slug'))
->searchable()
->copyable()
->copyMessage(__('proposal.link_copied'))
->icon('heroicon-o-document-duplicate')
->limit(25),
TextColumn::make('valid_until')
->label(__('proposal.table_valid_until'))
->date('d.m.Y')
->sortable(),
TextColumn::make('views_count')
->label(__('proposal.table_views'))
->sortable()
->alignCenter(),
TextColumn::make('created_at')
->label(__('proposal.table_created_at'))
->dateTime('d.m.Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('status')
->label(__('proposal.status_field'))
->options([
'draft' => __('proposal.status_draft'),
'sent' => __('proposal.status_sent'),
'accepted' => __('proposal.status_accepted'),
'rejected' => __('proposal.status_rejected'),
'revised' => __('proposal.status_revised'),
]),
])
->recordActions([
EditAction::make()
->label(__('proposal.edit')),
])
->actions([
Action::make('view_proposal')
->label(__('proposal.preview'))
->icon('heroicon-o-eye')
->url(fn ($record) => route('proposals.show', $record->slug))
->openUrlInNewTab(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->label(__('proposal.delete')),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -18,6 +18,30 @@ class EditSetting extends EditRecord
return __('settings.edit');
}
protected function mutateFormDataBeforeFill(array $data): array
{
$record = $this->getRecord();
if (isset($data['type'])) {
$type = $data['type'];
// Map the setting type to the form field name
$virtualField = match ($type) {
'string', 'text', 'json', 'integer', 'float' => 'value_text',
default => 'value_' . $type,
};
try {
// Model accessor'ını kullanarak değeri al
$data[$virtualField] = $record->{$virtualField};
} catch (\Exception $e) {
// Accessor bulunamazsa devam et
}
}
return $data;
}
protected function getHeaderActions(): array
{
return [
@@ -42,6 +42,7 @@ class SettingForm
->regex('/^[a-z0-9_-]+$/')
->maxLength(255)
->disabled(fn ($record) => $record !== null)
->live(onBlur: true)
->columnSpanFull(),
TextInput::make('label')
@@ -114,10 +115,28 @@ class SettingForm
->label(__('settings.value'))
->helperText(__('settings.value_helper'))
->required()
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json']))
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json', 'integer', 'float']) && !in_array($get('key'), ['default_header', 'default_footer']))
->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3)
->columnSpanFull(),
Select::make('value_header_template')
->label('Default Header Template')
->options(\App\Models\HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => $get('key') === 'default_header')
->statePath('value')
->searchable()
->preload()
->columnSpanFull(),
Select::make('value_footer_template')
->label('Default Footer Template')
->options(\App\Models\FooterTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => $get('key') === 'default_footer')
->statePath('value')
->searchable()
->preload()
->columnSpanFull(),
Toggle::make('value_boolean')
->label(__('settings.value'))
->helperText(__('settings.value_helper'))
@@ -134,7 +153,15 @@ class SettingForm
->directory('settings')
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
->maxSize(10240) // 10MB
->columnSpanFull(),
->openable()
->downloadable()
->previewable(true)
->columnSpanFull()
->afterStateHydrated(function ($component, $state, $record) {
if ($record && $record->type === 'file') {
$component->state($record->value);
}
}),
DatePicker::make('value_date')
->label(__('settings.value'))
@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSiteTranslation extends CreateRecord
{
protected static string $resource = SiteTranslationResource::class;
}
@@ -0,0 +1,24 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditSiteTranslation extends EditRecord
{
protected static string $resource = SiteTranslationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Actions\Action;
use Illuminate\Contracts\View\View;
class ListSiteTranslations extends ListRecords
{
protected static string $resource = SiteTranslationResource::class;
public bool $useDataGrid = false;
public function mount(): void
{
parent::mount();
// Session'dan görünüm tercihini al, yoksa varsayılan olarak false (eski görünüm)
$this->useDataGrid = session('site_translations_use_datagrid', false);
}
protected function getHeaderActions(): array
{
$useDataGrid = session('site_translations_use_datagrid', false);
return [
Action::make('toggleView')
->label($useDataGrid ? 'Tablo Görünümü' : 'DataGrid Görünümü')
->icon($useDataGrid ? 'heroicon-o-table-cells' : 'heroicon-o-squares-2x2')
->color('gray')
->action(function () {
$currentValue = session('site_translations_use_datagrid', false);
$newValue = !$currentValue;
session(['site_translations_use_datagrid' => $newValue]);
session()->save(); // Session'ı hemen kaydet
// Sayfayı yenile
$this->redirect($this->getResource()::getUrl('index'));
}),
CreateAction::make()
->visible(!$useDataGrid), // Sadece eski görünümde göster
];
}
public function render(): View
{
$useDataGrid = session('site_translations_use_datagrid', false);
if ($useDataGrid) {
return view('filament.admin.resources.site-translations.datagrid')
->layout($this->getLayout());
}
return parent::render();
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Schemas;
use App\Models\Language;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class SiteTranslationForm
{
public static function make(Schema $schema): Schema
{
$languages = Language::where('is_active', true)->get();
$translationFields = [];
foreach ($languages as $language) {
$translationFields[] = Textarea::make($language->code)
->label($language->name . " ({$language->code})")
->rows(2);
}
return $schema
->components([
Section::make()
->schema([
Textarea::make('key')
->label('Key / Original Text')
->required()
->columnSpanFull()
->helperText('The original text to be translated. Use this exact text in t() helper.'),
Group::make()
->schema($translationFields)
->statePath('translations')
->columnSpanFull(),
])
]);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations;
use App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\Schemas\SiteTranslationForm;
use App\Filament\Admin\Resources\SiteTranslations\Tables\SiteTranslationsTable;
use App\Models\SiteTranslation;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;
use BackedEnum;
class SiteTranslationResource extends Resource
{
protected static ?string $model = SiteTranslation::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-language';
protected static UnitEnum|string|null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 100;
public static function form(Schema $schema): Schema
{
return SiteTranslationForm::make($schema);
}
public static function table(Table $table): Table
{
return SiteTranslationsTable::make($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSiteTranslations::route('/'),
'create' => Pages\CreateSiteTranslation::route('/create'),
'edit' => Pages\EditSiteTranslation::route('/{record}/edit'),
];
}
public static function getNavigationLabel(): string
{
return __('Site Translations');
}
public static function getModelLabel(): string
{
return __('Translation');
}
public static function getPluralModelLabel(): string
{
return __('Translations');
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Tables;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use App\Models\Language;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Actions\ImportAction;
use Filament\Actions\ExportAction;
use App\Filament\Imports\SiteTranslationImporter;
use App\Filament\Exports\SiteTranslationExporter;
class SiteTranslationsTable
{
public static function make(Table $table): Table
{
$defaultLocale = app()->getLocale();
return $table
->headerActions([
ImportAction::make()
->importer(SiteTranslationImporter::class),
ExportAction::make()
->exporter(SiteTranslationExporter::class)
])
->columns([
TextColumn::make('key')
->searchable()
->sortable()
->limit(50)
->wrap(),
TextColumn::make("translations.{$defaultLocale}")
->label("Translation ({$defaultLocale})")
->searchable()
->limit(50)
->wrap()
->placeholder('No translation'),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make()
->modalWidth('xl')
->form(function () {
$languages = Language::where('is_active', true)->get();
$translationFields = [];
foreach ($languages as $language) {
$translationFields[] = Textarea::make($language->code)
->label($language->name . " ({$language->code})")
->rows(2);
}
return [
Section::make()
->schema([
Textarea::make('key')
->label('Key / Original Text')
->required()
->columnSpanFull()
->helperText('The original text to be translated. Use this exact text in t() helper.'),
Group::make()
->schema($translationFields)
->statePath('translations')
->columnSpanFull(),
])
];
}),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Exports;
use App\Models\SiteTranslation;
use App\Models\Language;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
class SiteTranslationExporter extends Exporter
{
protected static ?string $model = SiteTranslation::class;
public static function getColumns(): array
{
$columns = [
ExportColumn::make('key')->label('Key'),
];
// Veritabanı bağlantısı yoksa veya tablo yoksa hata vermemesi için try-catch veya kontrol
try {
$languages = Language::where('is_active', true)->get();
foreach ($languages as $language) {
$columns[] = ExportColumn::make("translations.{$language->code}")
->label($language->name . " ({$language->code})");
}
} catch (\Exception $e) {
// Migration henüz çalışmadıysa
}
return $columns;
}
public static function getCompletedNotificationBody(Export $export): string
{
$body = 'Your site translation export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
if ($failedRowsCount = $export->getFailedRowsCount()) {
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
}
return $body;
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Filament\Imports;
use App\Models\SiteTranslation;
use App\Models\Language;
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
use Filament\Actions\Imports\Models\Import;
class SiteTranslationImporter extends Importer
{
protected static ?string $model = SiteTranslation::class;
public static function getColumns(): array
{
$columns = [
ImportColumn::make('key')
->label('Key')
->requiredMapping()
->rules(['required']),
];
try {
$languages = Language::where('is_active', true)->get();
foreach ($languages as $language) {
$columns[] = ImportColumn::make($language->code)
->label($language->name . " ({$language->code})");
}
} catch (\Exception $e) {
//
}
return $columns;
}
public function resolveRecord(): ?SiteTranslation
{
if (empty($this->data['key'])) {
return null;
}
// Key üzerinden kaydı bul veya yeni oluştur
// Not: key alanı text olduğu için tam eşleşme arıyoruz
return SiteTranslation::firstOrNew([
'key' => $this->data['key'],
]);
}
protected function beforeSave(): void
{
$translations = $this->record->translations ?? [];
$languages = Language::where('is_active', true)->pluck('code')->toArray();
foreach ($languages as $code) {
if (array_key_exists($code, $this->data)) {
$value = $this->data[$code];
// Boş string de olsa güncelleyelim, null ise (CSV'de yoksa) dokunmayalım
if ($value !== null) {
$translations[$code] = $value;
}
}
}
$this->record->translations = $translations;
// Hash'i manuel oluşturmaya gerek yok, model event'i halledecek ama
// new record ise ve event çalışmazsa diye garantiye alalım (import toplu insert yapabilir mi? Hayır record record işler)
if (!$this->record->exists && empty($this->record->hash)) {
$this->record->hash = md5($this->record->key);
}
}
public static function getCompletedNotificationBody(Import $import): string
{
$body = 'Your site translation import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.';
if ($failedRowsCount = $import->getFailedRowsCount()) {
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.';
}
return $body;
}
}
+32 -2
View File
@@ -1,6 +1,8 @@
<?php
use App\Models\Setting;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
if (!function_exists('setting')) {
/**
@@ -12,7 +14,35 @@ if (!function_exists('setting')) {
*/
function setting(string $key, $default = null)
{
return Setting::get($key, $default);
// Cache settings to avoid database queries on every call
// Cache will store the full setting object to access type info
$setting = Cache::remember("app_setting_{$key}", 60 * 60 * 24, function () use ($key) {
return Setting::where('key', $key)
->where('is_active', true)
->first();
});
if (!$setting) {
return $default;
}
$value = $setting->value;
// Handle file/image types
if (in_array($setting->type, ['file', 'image']) || $key === 'home_hero') {
if (empty($value)) {
return $default;
}
// If value is already a URL, return it
if (filter_var($value, FILTER_VALIDATE_URL)) {
return $value;
}
// Return storage URL
return Storage::url($value);
}
return $value;
}
}
+129 -1
View File
@@ -2,6 +2,7 @@
use App\Models\Language;
use App\Models\Translation;
use App\Models\SiteTranslation;
use Illuminate\Support\Collection;
if (!function_exists('current_language')) {
@@ -221,6 +222,93 @@ if (!function_exists('get_translation_status_color')) {
}
}
if (!function_exists('flag_emoji_from_country_code')) {
/**
* Build a flag emoji from a two-letter country/region code (e.g. TR 🇹🇷).
*/
function flag_emoji_from_country_code(string $countryCode): string
{
$countryCode = strtoupper(substr($countryCode, 0, 2));
$flag = '';
for ($i = 0; $i < strlen($countryCode); $i++) {
$flag .= mb_chr(127397 + ord($countryCode[$i]));
}
return $flag;
}
}
if (!function_exists('country_code_from_flag_emoji')) {
/**
* Extract a two-letter code from a flag emoji (e.g. 🇬🇧 GB).
*/
function country_code_from_flag_emoji(?string $flag): ?string
{
if (empty($flag)) {
return null;
}
$code = '';
foreach (mb_str_split($flag) as $char) {
$ord = mb_ord($char);
if ($ord >= 0x1F1E6 && $ord <= 0x1F1FF) {
$code .= chr(ord('A') + ($ord - 0x1F1E6));
}
}
return strlen($code) === 2 ? $code : null;
}
}
if (!function_exists('resolve_language_flag')) {
/**
* Resolve flag emoji for a language (stored flag or generated from code).
*/
function resolve_language_flag(Language|string $language): string
{
if (is_string($language)) {
$language = Language::findByCode($language);
}
if (! $language) {
return '🌐';
}
if (! empty($language->flag) && $language->flag !== '🌐') {
return $language->flag;
}
return flag_emoji_from_country_code($language->code);
}
}
if (!function_exists('get_language_short_code')) {
/**
* Short display code for language (from flag region, e.g. GB, TR, SE).
*/
function get_language_short_code(Language|string $language): string
{
if (is_string($language)) {
$model = Language::findByCode($language);
return $model ? get_language_short_code($model) : strtoupper($language);
}
$fromFlag = country_code_from_flag_emoji($language->flag);
if ($fromFlag) {
return $fromFlag;
}
$fromResolved = country_code_from_flag_emoji(resolve_language_flag($language));
return $fromResolved ?: strtoupper($language->code);
}
}
if (!function_exists('get_language_flag')) {
/**
* Get language flag emoji or icon
@@ -228,7 +316,8 @@ if (!function_exists('get_language_flag')) {
function get_language_flag(string $languageCode): string
{
$language = Language::findByCode($languageCode);
return $language ? ($language->flag ?? '🌐') : '🌐';
return $language ? resolve_language_flag($language) : '🌐';
}
}
@@ -279,3 +368,42 @@ if (!function_exists('get_translation_value')) {
}
}
if (!function_exists('t')) {
/**
* Get site translation for a given key.
*
* @param string $key
* @return string
*/
function t(string $key): string
{
if (empty($key)) {
return '';
}
return SiteTranslation::getTranslation($key);
}
}
if (!function_exists('locale_upper')) {
/**
* Locale-aware uppercase (Turkish: i→İ, ı→I, etc.)
*/
function locale_upper(string $text, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
if (str_starts_with($locale, 'tr')) {
return mb_strtoupper(
str_replace(
['i', 'ı', 'ğ', 'ü', 'ş', 'ö', 'ç'],
['İ', 'I', 'Ğ', 'Ü', 'Ş', 'Ö', 'Ç'],
$text
),
'UTF-8'
);
}
return mb_strtoupper($text, 'UTF-8');
}
}
@@ -0,0 +1,292 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SiteTranslation;
use App\Models\Language;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class SiteTranslationController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$query = SiteTranslation::query();
// Search
if ($request->has('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('key', 'like', "%{$search}%")
->orWhereRaw("JSON_SEARCH(translations, 'one', ?, NULL, '$[*]') IS NOT NULL", ["%{$search}%"]);
});
}
// Sorting
$sortField = $request->get('sort', 'id');
$sortOrder = $request->get('order', 'desc');
$query->orderBy($sortField, $sortOrder);
// Pagination
$page = $request->get('page', 1);
$pageSize = $request->get('pageSize', 20);
$totalCount = $query->count();
$items = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get();
// Format data for DataGrid
$data = $items->map(function ($item) {
$formatted = [
'id' => $item->id,
'key' => $item->key,
'hash' => $item->hash,
'created_at' => $item->created_at?->toDateTimeString(),
'updated_at' => $item->updated_at?->toDateTimeString(),
];
// Add translation fields for each active language
$languages = Language::where('is_active', true)->get();
$translations = $item->translations ?? [];
foreach ($languages as $language) {
$formatted[$language->code] = $translations[$language->code] ?? '';
}
return $formatted;
});
return response()->json([
'data' => $data,
'totalCount' => $totalCount,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'key' => 'required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$hash = md5($request->key);
// Check if translation already exists
$exists = SiteTranslation::where('hash', $hash)->first();
if ($exists) {
return response()->json([
'success' => false,
'message' => 'Bu key zaten mevcut.',
], 409);
}
$languages = Language::where('is_active', true)->get();
$translations = [];
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
$translation = SiteTranslation::create([
'hash' => $hash,
'key' => $request->key,
'translations' => $translations,
]);
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'data' => $translation->load('translations'),
'message' => 'Çeviri başarıyla oluşturuldu.',
], 201);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$validator = Validator::make($request->all(), [
'key' => 'sometimes|required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
// Update key if provided
if ($request->has('key') && $request->key !== $translation->key) {
$translation->key = $request->key;
$translation->hash = md5($request->key);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
return response()->json([
'success' => true,
'data' => $translation->fresh(),
'message' => 'Çeviri başarıyla güncellendi.',
]);
}
/**
* Batch update multiple resources.
*/
public function batchUpdate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'changes' => 'required|array',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$changes = $request->get('changes', []);
$updated = [];
$errors = [];
foreach ($changes as $change) {
try {
// ID can be in 'key' field (integer) or directly in 'key' field
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? null);
if (!$id) {
$errors[] = 'ID bulunamadı.';
continue;
}
$translation = SiteTranslation::find($id);
if (!$translation) {
$errors[] = "ID {$id} için kayıt bulunamadı.";
continue;
}
$data = $change['data'] ?? [];
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if (isset($data[$language->code])) {
$translations[$language->code] = $data[$language->code];
}
}
// Update key if provided
if (isset($data['key']) && $data['key'] !== $translation->key) {
$translation->key = $data['key'];
$translation->hash = md5($data['key']);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
$updated[] = $translation->id;
} catch (\Exception $e) {
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? 'unknown');
$errors[] = "ID {$id} için hata: " . $e->getMessage();
}
}
return response()->json([
'success' => count($errors) === 0,
'updated' => $updated,
'errors' => $errors,
'message' => count($updated) . ' kayıt güncellendi.',
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$hash = $translation->hash;
$translation->delete();
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'message' => 'Çeviri başarıyla silindi.',
]);
}
/**
* Batch delete multiple resources.
*/
public function batchDestroy(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'ids' => 'required|array',
'ids.*' => 'required|integer|exists:site_translations,id',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$ids = $request->get('ids', []);
$translations = SiteTranslation::whereIn('id', $ids)->get();
foreach ($translations as $translation) {
Cache::forget("site_translation_{$translation->hash}");
}
SiteTranslation::whereIn('id', $ids)->delete();
return response()->json([
'success' => true,
'message' => count($ids) . ' kayıt başarıyla silindi.',
]);
}
}
+162 -15
View File
@@ -8,6 +8,8 @@ use App\Models\BlogComment;
use App\Models\HeaderTemplate;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use App\Models\Page;
use App\Support\BlogStructuredData;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
@@ -40,47 +42,179 @@ class BlogController extends Controller
$query = $query->whereJsonContains('tags', $request->tag);
}
// Search filtresi
if ($query && $request->has('search') && $request->search) {
$search = $request->search;
$query = $query->where(function($q) use ($search) {
$q->where('title', 'like', '%' . $search . '%')
->orWhere('content', 'like', '%' . $search . '%')
->orWhere('excerpt', 'like', '%' . $search . '%');
});
}
$posts = $query
? $query->latest('published_at')->paginate(12)
: collect();
// Render Header Template - En son aktif olan header template'i kullan
// Blog sayfa ayarlarını bul (varsa)
$blogPage = Page::where('slug', 'blog')->first();
// Render Header Template
$renderedHeader = null;
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
$headerTemplate = null;
// 1. Öncelik: Blog sayfasında seçili header template
if ($blogPage && $blogPage->header_template_id) {
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
}
// 2. Öncelik: Varsayılan aktif template (KALDIRILDI - Default static header kullanılmalı)
/*
if (!$headerTemplate) {
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
*/
if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$templateDefaults
$mergedData,
$blogPage
);
}
// Render Footer Template - En son aktif olan footer template'i kullan
// Render Footer Template
$renderedFooter = null;
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
$footerTemplate = null;
// 1. Öncelik: Blog sayfasında seçili footer template
if ($blogPage && $blogPage->footer_template_id) {
$footerTemplate = FooterTemplate::find($blogPage->footer_template_id);
}
// 2. Öncelik: Varsayılan aktif template
if (!$footerTemplate) {
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->footer_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$templateDefaults
$mergedData,
$blogPage
);
}
// Kategorileri al (filtreleme için)
$categories = class_exists(\App\Models\BlogCategory::class) ? \App\Models\BlogCategory::where('is_active', true)->orderBy('sort_order')->get() : collect();
// Elemis Widget'ları için veri hazırlığı
$carouselPosts = collect();
$highlightCategories = collect();
$popularPosts = collect();
$sidebarCategories = collect();
$tags = collect();
if (!$request->ajax()) {
if (class_exists(Blog::class)) {
// Karusel: Öne çıkarılan ya da en güncel 5 yazı
$carouselPosts = Blog::with(['category', 'author'])
->published()
->featured()
->latest('published_at')
->take(5)
->get();
if ($carouselPosts->isEmpty()) {
$carouselPosts = Blog::with(['category', 'author'])
->published()
->latest('published_at')
->take(5)
->get();
}
// Popüler Yazılar: En çok okunan 3 yazı
$popularPosts = Blog::with(['category', 'author'])
->published()
->orderBy('view_count', 'desc')
->take(3)
->get();
// Etiket Bulutu: Tüm yayınlanan yazılardaki benzersiz etiketler
$allTags = Blog::published()
->whereNotNull('tags')
->pluck('tags');
$tagsCollected = collect();
foreach ($allTags as $postTags) {
if (is_array($postTags)) {
foreach ($postTags as $tag) {
$tagsCollected->push($tag);
}
}
}
$tags = $tagsCollected->unique()->take(15);
}
if (class_exists(\App\Models\BlogCategory::class)) {
// Hoş Geldiniz bölümü altındaki 4'lü kategori vurgusu
$highlightCategories = \App\Models\BlogCategory::where('is_active', true)
->withCount('blogs')
->has('blogs')
->orderBy('sort_order')
->take(4)
->get();
if ($highlightCategories->count() < 4) {
$highlightCategories = \App\Models\BlogCategory::where('is_active', true)
->orderBy('sort_order')
->take(4)
->get();
}
// Sidebar Kategorileri: Yazı sayısıyla birlikte
$sidebarCategories = \App\Models\BlogCategory::where('is_active', true)
->withCount('blogs')
->orderBy('sort_order')
->get();
}
}
if ($request->ajax()) {
return view('blog.partials.posts', [
'posts' => $posts
]);
}
$pageUrl = url()->current();
return view('blog.index', [
'settings' => $settings,
'posts' => $posts,
'categories' => $categories,
'carouselPosts' => $carouselPosts,
'highlightCategories' => $highlightCategories,
'popularPosts' => $popularPosts,
'sidebarCategories' => $sidebarCategories,
'tags' => $tags,
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => [
'title' => __('blog.meta-index-title'),
'description' => __('blog.meta-index-description'),
],
'structuredData' => $posts instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator
? BlogStructuredData::forIndex($posts, $pageUrl)
: null,
]);
}
@@ -132,17 +266,26 @@ class BlogController extends Controller
->orderBy('created_at', 'desc')
->get();
// Render Header Template - En son aktif olan header template'i kullan
// Blog sayfa ayarlarını bul (varsa)
$blogPage = Page::where('slug', 'blog')->first();
// Render Header Template
$renderedHeader = null;
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
$headerTemplate = null;
// 1. Öncelik: Blog sayfasında seçili header template
if ($blogPage && $blogPage->header_template_id) {
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
}
if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$templateDefaults,
$mergedData,
$post
);
}
@@ -162,6 +305,8 @@ class BlogController extends Controller
);
}
$pageUrl = route('blog.show', $post->slug);
return view('blog.show', [
'settings' => $settings,
'post' => $post,
@@ -173,7 +318,9 @@ class BlogController extends Controller
'title' => method_exists($post, 'translate') ? ($post->translate('meta_title') ?: $post->translate('title')) : ($post->meta_title ?? $post->title),
'description' => method_exists($post, 'translate') ? ($post->translate('meta_description') ?: $post->translate('excerpt')) : ($post->meta_description ?? $post->excerpt),
'image' => $post->featured_image ? asset('storage/' . $post->featured_image) : null,
'og_type' => 'article',
],
'structuredData' => BlogStructuredData::forShow($post, $pageUrl),
]);
}
+744
View File
@@ -0,0 +1,744 @@
<?php
namespace App\Http\Controllers;
use App\Models\CareerApplication;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class CareerController extends Controller
{
public function index()
{
return view('front.career.index', [
'meta' => [
'title' => __('career.job_application_title', ['default' => 'İş Başvurusu']),
'description' => __('career.job_application_description', ['default' => 'Ekibimize katılmak için aşağıdaki formu doldurarak CV\'nizi iletebilirsiniz.']),
]
]);
}
public function internship()
{
return view('front.career.internship', [
'meta' => [
'title' => __('career.internship_title', ['default' => 'Staj Başvurusu']),
'description' => __('career.internship_description', ['default' => 'Uzaktan staj programımız hakkında bilgi alın ve başvurunuzu yapın.']),
]
]);
}
public function store(Request $request)
{
$rules = [
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'nullable|string|max:20',
'cv' => 'required|file|mimes:pdf,doc,docx|max:5120', // Max 5MB
'message' => 'nullable|string',
];
if ($request->type === 'job') {
$rules['nda'] = 'required|file|mimes:pdf|max:5120';
$rules['contract'] = 'required|file|mimes:pdf|max:5120';
$rules['id_photocopy'] = 'required|file|mimes:pdf,jpg,jpeg,png|max:5120'; // Max 5MB
$rules['git_knowledge'] = 'accepted';
$rules['ai_usage'] = 'accepted';
}
$request->validate($rules);
$cvPath = $request->file('cv') ? $request->file('cv')->store('cvs', 'public') : null;
$ndaPath = $request->file('nda') ? $request->file('nda')->store('ndas', 'public') : null;
$contractPath = $request->file('contract') ? $request->file('contract')->store('contracts', 'public') : null;
$idPhotocopyPath = $request->file('id_photocopy') ? $request->file('id_photocopy')->store('id_photocopies', 'public') : null;
CareerApplication::create([
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone,
'type' => $request->type ?? 'job',
'cv_path' => $cvPath,
'nda_path' => $ndaPath,
'contract_path' => $contractPath,
'id_photocopy_path' => $idPhotocopyPath,
'git_knowledge' => $request->has('git_knowledge'),
'ai_usage' => $request->has('ai_usage'),
'message' => $request->message,
'status' => 'pending',
]);
if ($request->ajax()) {
return response()->json([
'success' => true,
'message' => __('career.success_message')
]);
}
return redirect()->back()->with('success', __('career.success_message'));
}
public function internLoginForm()
{
if (session()->has('intern_id')) {
return redirect()->route('intern.dashboard');
}
return view('front.career.intern_login', [
'meta' => [
'title' => 'Stajyer Girişi',
'description' => 'İmzalı staj belgelerinize erişmek için lütfen giriş yapın.',
]
]);
}
public function internLogin(Request $request)
{
$request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
$intern = CareerApplication::where('username', $request->username)
->where('type', 'internship')
->first();
if (!$intern || !\Illuminate\Support\Facades\Hash::check($request->password, $intern->password)) {
return redirect()->back()
->withInput($request->only('username'))
->withErrors([
'username' => 'Girdiğiniz kullanıcı adı veya şifre hatalı.',
]);
}
session(['intern_id' => $intern->id]);
return redirect()->route('intern.dashboard')->with('success', 'Başarıyla giriş yapıldı.');
}
public function internDashboard()
{
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen giriş yapın.');
}
$intern = CareerApplication::findOrFail(session('intern_id'));
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.intern_dashboard', [
'intern' => $intern,
'days' => $days,
'savedEntries' => $savedEntries,
'meta' => [
'title' => 'Stajyer Paneli',
'description' => 'Belgelerinizi buradan indirebilirsiniz.',
]
]);
}
public function uploadInternshipForm(Request $request)
{
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$request->validate([
'internship_form' => 'required|file|mimes:pdf,docx,jpg,png,jpeg|max:51200',
'internship_start_date' => 'required|date',
'internship_total_days' => 'required|integer|min:1',
], [
'internship_form.required' => 'Lütfen bir dosya seçin.',
'internship_form.file' => 'Yüklenen öğe geçerli bir dosya olmalıdır.',
'internship_form.mimes' => 'Yalnızca PDF, DOCX, JPG ve PNG formatındaki dosyalar kabul edilir.',
'internship_form.max' => 'Dosya boyutu en fazla 50MB olabilir.',
'internship_start_date.required' => 'Lütfen staj başlangıç tarihini girin.',
'internship_start_date.date' => 'Geçerli bir başlangıç tarihi girin.',
'internship_total_days.required' => 'Lütfen staj süresini girin.',
'internship_total_days.integer' => 'Staj süresi geçerli bir tam sayı olmalıdır.',
'internship_total_days.min' => 'Staj süresi en az 1 gün olmalıdır.',
]);
$intern = CareerApplication::findOrFail(session('intern_id'));
if ($request->hasFile('internship_form')) {
// Delete old file if exists
if ($intern->to_be_signed_internship_form_path) {
Storage::disk('public')->delete($intern->to_be_signed_internship_form_path);
}
$path = $request->file('internship_form')->store('to_be_signed_interns', 'public');
// Calculate end date based on weekdays and duration
$startDate = \Carbon\Carbon::parse($request->internship_start_date);
$daysToAdd = intval($request->internship_total_days);
$endDate = $startDate->copy();
$count = 0;
$temp = $startDate->copy();
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
$temp->addDay();
continue;
}
$endDate = $temp->copy();
$temp->addDay();
$count++;
}
$intern->update([
'to_be_signed_internship_form_path' => $path,
'internship_start_date' => $request->internship_start_date,
'internship_end_date' => $endDate->format('Y-m-d'),
'internship_total_days' => $daysToAdd,
'status' => 'waiting_document',
]);
return redirect()->back()->with('success', 'İmzalanacak staj formunuz ve staj tarihleri başarıyla kaydedildi.');
}
return redirect()->back()->with('error', 'Dosya yüklenirken bir hata oluştu.');
}
public function saveGithubRepo(Request $request)
{
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$request->validate([
'github_repo' => 'required|string|url|max:255',
'github_username' => 'nullable|string|max:100',
], [
'github_repo.required' => 'Lütfen Github depo URL\'sini girin.',
'github_repo.url' => 'Geçerli bir URL girilmelidir.',
'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.',
'github_username.max' => 'Github kullanıcı adı en fazla 100 karakter olabilir.',
]);
$intern = CareerApplication::findOrFail(session('intern_id'));
// Sanitize & format github URL
$repoUrl = trim($request->github_repo);
$username = trim($request->github_username);
$intern->update([
'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]);
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => 'Github bilgileri başarıyla güncellendi.',
'github_repo' => $repoUrl,
'github_username' => $username ?: null,
]);
}
return redirect()->back()->with('success', 'Github bilgileri başarıyla güncellendi.');
}
public function downloadMarkdown()
{
if (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
$internId = request('intern_id');
} else {
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$internId = session('intern_id');
}
$intern = CareerApplication::findOrFail($internId);
$repo = $intern->github_repo;
if (!$repo) {
return redirect()->back()->with('error', 'Lütfen önce Github deposunu tanımlayın.');
}
// Parse owner and repo
// E.g., https://github.com/owner/repo or owner/repo
$repoClean = str_replace(['https://github.com/', 'http://github.com/', 'https://www.github.com/', 'http://www.github.com/'], '', $repo);
$repoClean = trim($repoClean, '/');
$parts = explode('/', $repoClean);
if (count($parts) < 2) {
return redirect()->back()->with('error', 'Geçersiz Github depo formatı.');
}
$owner = $parts[0];
$repoName = explode('.', $parts[1])[0]; // Remove .git if exists
// Fetch commits
$url = "https://api.github.com/repos/{$owner}/{$repoName}/commits?per_page=100";
$opts = [
'http' => [
'method' => 'GET',
'header' => [
'User-Agent: PHP-Github-Client',
]
]
];
$context = stream_context_create($opts);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
return redirect()->back()->with('error', 'Github API\'den commit bilgileri alınamadı. Deponuzun public olduğundan emin olun.');
}
$commits = json_decode($response, true);
if (!is_array($commits)) {
return redirect()->back()->with('error', 'Github API yanıtı çözümlenemedi.');
}
// Sort commits chronological (ascending)
$commits = array_reverse($commits);
// Group commits by date (YYYY-MM-DD)
$grouped = [];
foreach ($commits as $c) {
$dateStr = $c['commit']['author']['date'] ?? '';
if ($dateStr) {
$dateOnly = \Carbon\Carbon::parse($dateStr)->format('Y-m-d');
$grouped[$dateOnly][] = $c;
}
}
// Sort dates chronological (ascending)
ksort($grouped);
// Generate Markdown content
$md = "# Staj Günlüğü - " . $intern->name . "\n";
$md .= "Depo: https://github.com/{$owner}/{$repoName}\n\n";
$day = 1;
foreach ($grouped as $date => $dayCommits) {
$formattedDate = \Carbon\Carbon::parse($date)->format('d.m.Y');
$md .= "## {$day}. Gün ({$formattedDate})\n";
foreach ($dayCommits as $c) {
$message = trim($c['commit']['message'] ?? '');
$time = \Carbon\Carbon::parse($c['commit']['author']['date'] ?? '')->format('H:i');
$hash = substr($c['sha'] ?? '', 0, 7);
$md .= "- [{$time}] `{$hash}`: {$message}\n";
}
$md .= "\n";
$day++;
}
$fileName = str()->slug($intern->name) . "-staj-gunlugu.md";
return response($md, 200, [
'Content-Type' => 'text/markdown; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
]);
}
public function internLogout()
{
session()->forget('intern_id');
return redirect()->route('intern.login')->with('success', 'Başarıyla çıkış yapıldı.');
}
public function verifyCertificate($code)
{
$application = CareerApplication::where('certificate_code', $code)
->where('type', 'internship')
->firstOrFail();
$days = self::getInternshipDates($application->internship_start_date, $application->internship_total_days);
$savedEntries = $application->journalEntries()->get()->keyBy('day_number');
return view('front.career.verify', [
'application' => $application,
'days' => $days,
'savedEntries' => $savedEntries,
'meta' => [
'title' => 'Staj Bitirme Sertifikası Doğrulama - ' . $application->name,
'description' => $application->name . ' isimli stajyerimizin staj bitirme sertifikası ve performans raporu doğrulama sayfası.',
]
]);
}
public function internAdminLoginForm()
{
if (auth()->check() && auth()->user()->hasRole('super_admin')) {
return redirect()->route('intern.admin.dashboard');
}
return view('front.career.admin_login', [
'meta' => [
'title' => 'Yönetici Girişi - Staj Takip',
'description' => 'Stajyerleri yönetmek için giriş yapın.',
]
]);
}
public function internAdminLogin(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
if (auth()->attempt($request->only('email', 'password'))) {
if (auth()->user()->hasRole('super_admin')) {
return redirect()->route('intern.admin.dashboard')->with('success', 'Yönetici girişi başarıyla gerçekleştirildi.');
}
auth()->logout();
return redirect()->back()
->withInput($request->only('email'))
->withErrors([
'email' => 'Bu panele erişmek için super_admin yetkinizin olması gerekir.',
]);
}
return redirect()->back()
->withInput($request->only('email'))
->withErrors([
'email' => 'Girdiğiniz bilgiler eşleşmedi veya hatalı.',
]);
}
public function internAdminDashboard()
{
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
return redirect()->route('intern.admin.login')->with('error', 'Lütfen önce yönetici olarak giriş yapın.');
}
$allInterns = CareerApplication::where('type', 'internship')
->with(['journalEntries'])
->orderBy('created_at', 'desc')
->get();
$ganttInterns = CareerApplication::where('type', 'internship')
->where('status', 'accepted')
->whereNotNull('internship_start_date')
->whereNotNull('internship_end_date')
->orderBy('internship_start_date', 'asc')
->get()
->map(function ($intern) {
return [
'id' => $intern->id,
'parentId' => null,
'title' => $intern->name,
'start' => $intern->internship_start_date,
'end' => $intern->internship_end_date,
'progress' => 100
];
});
return view('front.career.admin_dashboard', [
'allInterns' => $allInterns,
'ganttInterns' => $ganttInterns,
'meta' => [
'title' => 'Staj Yönetici Paneli',
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
]
]);
}
public function internAdminLogout()
{
auth()->logout();
return redirect()->route('intern.admin.login')->with('success', 'Yönetici oturumu sonlandırıldı.');
}
public static function getInternshipDates($startDate, $totalDays)
{
if (!$startDate || !$totalDays || $totalDays <= 0) {
return [];
}
$dates = [];
$temp = \Carbon\Carbon::parse($startDate);
$daysToAdd = intval($totalDays);
$count = 0;
while ($count < $daysToAdd) {
if ($temp->isWeekend()) {
$temp->addDay();
continue;
}
$dates[] = [
'day_number' => $count + 1,
'date' => $temp->format('Y-m-d'),
'formatted_date' => $temp->format('d.m.Y'),
];
$temp->addDay();
$count++;
}
return $dates;
}
public function saveJournalEntry(Request $request)
{
if (!session()->has('intern_id')) {
return response()->json(['success' => false, 'message' => 'Lütfen giriş yapın.'], 401);
}
$intern = CareerApplication::findOrFail(session('intern_id'));
$request->validate([
'day_number' => 'required|integer|min:1',
'date' => 'required|date',
'content' => 'nullable|string',
]);
$dateObj = \Carbon\Carbon::parse($request->date);
// 1. Prevent future entries
if ($dateObj->isFuture()) {
return response()->json(['success' => false, 'message' => 'İleriye dönük staj günleri için defter doldurulamaz.'], 422);
}
// 2. Prevent weekend entries
if ($dateObj->isWeekend()) {
return response()->json(['success' => false, 'message' => 'Hafta sonu günlerine staj günlüğü girilemez.'], 422);
}
// 3. Determine if retroactive
$isRetroactive = $dateObj->lt(\Carbon\Carbon::today());
$entry = \App\Models\InternshipJournalEntry::updateOrCreate([
'career_application_id' => $intern->id,
'day_number' => $request->day_number,
'date' => $request->date,
], [
'content' => $request->content,
'is_retroactive' => $isRetroactive,
'supervisor_approved' => false,
'supervisor_name' => null,
]);
return response()->json([
'success' => true,
'message' => $request->day_number . '. Gün kaydı başarıyla kaydedildi.' . ($isRetroactive ? ' (Geriye Dönük Kayıt)' : ''),
'entry' => $entry
]);
}
public function printJournal(Request $request)
{
if (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
$internId = request('intern_id');
} else {
if (!session()->has('intern_id')) {
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
}
$internId = session('intern_id');
}
$intern = CareerApplication::findOrFail($internId);
$size = $request->query('size', 'a4');
if (!in_array($size, ['a4', 'a5'])) {
$size = 'a4';
}
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayFilter = $request->query('day');
if ($dayFilter) {
$days = array_filter($days, function($d) use ($dayFilter) {
return $d['day_number'] == $dayFilter;
});
$days = array_values($days);
}
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
return view('front.career.print', [
'intern' => $intern,
'days' => $days,
'savedEntries' => $savedEntries,
'size' => $size,
]);
}
public function getJournalEntry(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
'date' => 'required|date',
]);
$intern = \App\Models\CareerApplication::findOrFail($request->intern_id);
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
$dayNum = null;
$dateFormatted = null;
foreach ($days as $d) {
if ($d['date'] === $request->date) {
$dayNum = $d['day_number'];
$dateFormatted = $d['formatted_date'];
break;
}
}
if (!$dayNum) {
return response()->json([
'success' => false,
'message' => 'Seçilen tarih staj dönemi dışındadır veya haftasonudur.'
], 422);
}
$entry = $intern->journalEntries()->where('date', $request->date)->first();
return response()->json([
'success' => true,
'intern_name' => $intern->name,
'day_number' => $dayNum,
'date' => $request->date,
'date_formatted' => $dateFormatted,
'entry' => $entry ? [
'id' => $entry->id,
'content' => $entry->content,
'is_retroactive' => $entry->is_retroactive,
'supervisor_approved' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
] : null
]);
}
public function toggleJournalApproval(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'entry_id' => 'required|integer|exists:internship_journal_entries,id',
]);
$entry = \App\Models\InternshipJournalEntry::findOrFail($request->entry_id);
$entry->supervisor_approved = !$entry->supervisor_approved;
if ($entry->supervisor_approved) {
$entry->supervisor_name = auth()->user()->name;
} else {
$entry->supervisor_name = null;
}
$entry->save();
return response()->json([
'success' => true,
'status' => $entry->supervisor_approved,
'supervisor_name' => $entry->supervisor_name,
'message' => 'Staj sorumlusu onayı güncellendi.'
]);
}
public function getInternJournalDetails(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
// Get all days calculated
$days = self::getInternshipDates($intern->internship_start_date, $intern->internship_total_days);
// Get saved journal entries
$savedEntries = $intern->journalEntries()->get()->keyBy('day_number');
// Prepare entries list matched by day number
$entries = [];
$filledDaysCount = 0;
foreach ($days as $d) {
$dayNum = $d['day_number'];
$entry = $savedEntries->get($dayNum);
$hasSaved = $entry && trim($entry->content) !== '';
if ($hasSaved) {
$filledDaysCount++;
}
$entries[] = [
'day_number' => $dayNum,
'date' => $d['date'],
'formatted_date' => $d['formatted_date'],
'filled' => $hasSaved,
'entry_id' => $entry ? $entry->id : null,
'content' => $entry ? $entry->content : null,
'is_retroactive' => $entry ? $entry->is_retroactive : false,
'supervisor_approved' => $entry ? (bool)$entry->supervisor_approved : false,
'supervisor_name' => $entry ? $entry->supervisor_name : null,
];
}
return response()->json([
'success' => true,
'intern' => [
'id' => $intern->id,
'name' => $intern->name,
'email' => $intern->email,
'phone' => $intern->phone,
'start_date' => $intern->internship_start_date,
'end_date' => $intern->internship_end_date,
'total_days' => $intern->internship_total_days,
'filled_days' => $filledDaysCount,
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
],
'entries' => $entries,
]);
}
public function toggleNotebookSignature(Request $request)
{
if (!auth()->check()) {
return response()->json(['success' => false, 'message' => 'Yetkisiz işlem.'], 403);
}
$request->validate([
'intern_id' => 'required|integer|exists:career_applications,id',
'type' => 'required|string|in:supervisor,unit,approved',
'signed' => 'required|boolean',
'name' => 'nullable|string|max:255',
]);
$intern = CareerApplication::findOrFail($request->intern_id);
$type = $request->type;
$signed = $request->signed;
$name = $request->name ?: auth()->user()->name;
if ($type === 'supervisor') {
$intern->notebook_supervisor_signed = $signed;
$intern->notebook_supervisor_name = $signed ? $name : null;
} elseif ($type === 'unit') {
$intern->notebook_unit_signed = $signed;
$intern->notebook_unit_name = $signed ? $name : null;
} elseif ($type === 'approved') {
$intern->notebook_approved = $signed;
}
$intern->save();
return response()->json([
'success' => true,
'message' => 'Staj defteri onay durumu güncellendi.',
'intern' => [
'notebook_supervisor_signed' => (bool)$intern->notebook_supervisor_signed,
'notebook_supervisor_name' => $intern->notebook_supervisor_name,
'notebook_unit_signed' => (bool)$intern->notebook_unit_signed,
'notebook_unit_name' => $intern->notebook_unit_name,
'notebook_approved' => (bool)$intern->notebook_approved,
]
]);
}
}
@@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers;
use App\Models\MusicProduction;
use App\Models\Setting;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use App\Support\MusicProductionStructuredData;
use Illuminate\Support\Str;
class MusicProductionController extends Controller
{
public function index()
{
// Get Settings
$settings = new \stdClass();
$allSettings = Setting::query()->where('is_active', true)->get();
foreach ($allSettings as $setting) {
$settings->{$setting->key} = $setting->value;
}
$search = trim((string) request('q', ''));
$productionsQuery = MusicProduction::active()->ordered();
if ($search !== '') {
$locale = app()->getLocale();
$productionsQuery->where(function ($query) use ($search, $locale) {
$query->where('title', 'like', "%{$search}%")
->orWhere('client_name', 'like', "%{$search}%")
->orWhere('slug', 'like', "%{$search}%")
->orWhereHas('translations', function ($translationQuery) use ($search, $locale) {
$translationQuery
->where('status', 'published')
->whereIn('field_name', ['title', 'client_name', 'content'])
->where('field_value', 'like', "%{$search}%")
->where('language_code', $locale);
});
});
}
$productions = $productionsQuery->paginate(9)->withQueryString();
// --- Footer Logic ---
$renderedFooter = null;
$defaultFooterId = $settings->default_footer ?? null;
if ($defaultFooterId) {
$footerTemplate = FooterTemplate::query()->find($defaultFooterId);
if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? [];
$mergedFooterData = array_merge($templateDefaults, []);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
null
);
}
}
$canonicalUrl = route('music-productions.index');
$pageUrl = $search !== '' ? $canonicalUrl : url()->current();
$meta = [
'title' => __('music_productions.meta-index-title'),
'description' => __('music_productions.meta-index-description'),
'image' => asset('assets/music-production.png'),
'image_alt' => __('music_productions.meta-index-title'),
'canonical' => $canonicalUrl,
'robots' => $search !== '' ? 'noindex, follow' : 'index, follow',
'og_type' => 'website',
'locale' => app()->getLocale(),
];
$structuredData = MusicProductionStructuredData::forIndex($productions, $pageUrl);
return view('front.music-productions.index', [
'productions' => $productions,
'search' => $search,
'settings' => $settings,
'header' => 'partials.header-center-nav',
'renderedFooter' => $renderedFooter,
'meta' => $meta,
'structuredData' => $structuredData,
]);
}
public function show($slug)
{
$production = MusicProduction::active()
->where('slug', $slug)
->firstOrFail();
// Get Settings
$settings = new \stdClass();
$allSettings = Setting::query()->where('is_active', true)->get();
foreach ($allSettings as $setting) {
$settings->{$setting->key} = $setting->value;
}
// Fetch adjacent posts (Prev and Next)
$prev = MusicProduction::active()
->where(function($q) use ($production) {
$q->where('sort_order', '<', $production->sort_order)
->orWhere(function($sub) use ($production) {
$sub->where('sort_order', $production->sort_order)
->where('id', '<', $production->id);
});
})
->orderBy('sort_order', 'desc')
->orderBy('id', 'desc')
->first();
$next = MusicProduction::active()
->where(function($q) use ($production) {
$q->where('sort_order', '>', $production->sort_order)
->orWhere(function($sub) use ($production) {
$sub->where('sort_order', $production->sort_order)
->where('id', '>', $production->id);
});
})
->orderBy('sort_order', 'asc')
->orderBy('id', 'asc')
->first();
// --- Footer Logic ---
$renderedFooter = null;
$defaultFooterId = $settings->default_footer ?? null;
if ($defaultFooterId) {
$footerTemplate = FooterTemplate::query()->find($defaultFooterId);
if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? [];
$mergedFooterData = array_merge($templateDefaults, []);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$production
);
}
}
$pageUrl = route('music-productions.show', $production->slug);
$title = $production->translate('title');
$description = Str::limit(strip_tags((string) $production->translate('content')), 160)
?: __('music_productions.meta-index-description');
$meta = [
'title' => $title,
'description' => $description,
'image' => $production->cover_image_url ?: asset('assets/music-production.png'),
'image_alt' => $title,
'canonical' => $pageUrl,
'robots' => 'index, follow',
'og_type' => 'article',
'locale' => app()->getLocale(),
];
$structuredData = MusicProductionStructuredData::forShow($production, $pageUrl);
return view('front.music-productions.show', [
'production' => $production,
'prev' => $prev,
'next' => $next,
'settings' => $settings,
'header' => 'partials.header-center-nav',
'renderedFooter' => $renderedFooter,
'meta' => $meta,
'structuredData' => $structuredData,
]);
}
}
+176 -151
View File
@@ -7,7 +7,10 @@ use App\Models\Setting;
use App\Models\HeaderTemplate;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use App\Support\PageStructuredData;
use App\Support\PageTemplateHero;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PageController extends Controller
{
@@ -16,217 +19,242 @@ class PageController extends Controller
*/
public function index()
{
// 1) is_homepage işaretli ve published (en son güncellenen)
$settings = $this->getSettings();
$page = Page::with(['headerTemplate', 'footerTemplate'])
->where('is_homepage', true)
->where('status', 'published')
->latest('updated_at')
->first();
// 1. Statik "home" Template Kontrolü
if (view()->exists('templates.home')) {
return $this->renderView('templates.home', $page, $settings);
}
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
// Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
// Hiç sayfa yoksa fallback
if (!$page) {
$meta = $this->getMeta(null, $settings);
return view('templates.home', [
'page' => null,
'settings' => $settings,
'meta' => [
'title' => $settings->default_meta_title ?? config('app.name'),
'description' => $settings->default_meta_description ?? null,
'image' => $settings->default_meta_image ?? null,
],
'meta' => $meta,
'structuredData' => PageStructuredData::forPage(null, url('/'), $meta['title'] ?? null, $meta['description'] ?? null),
]);
}
$metaTitle = method_exists($page, 'translate')
? ($page->translate('meta_title') ?: $page->translate('title'))
: ($page->meta_title ?? $page->title ?? null);
$metaDescription = method_exists($page, 'translate')
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
: ($page->meta_description ?? $page->excerpt ?? null);
// Section verisini al - Hem eski hem yeni sistemi destekle
$sections = $page->sections ?? $page->data ?? [];
// Yeni dinamik template sistemi için sections_data varsa onu da al
$templatedSections = $page->templated_sections ?? collect([]);
// Template belirleme mantığı:
// 1. Kullanıcı template seçmişse, onu kullan
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
// 3. Hiçbiri yoksa, 'generic' kullan
$template = $page->template
?? (($page->slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
$view = view()->exists("templates.$template")
? "templates.$template"
: 'templates.generic';
// Render Header Template
$renderedHeader = null;
$headerTemplate = $page->headerTemplate;
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
if (!$headerTemplate) {
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
if ($headerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $page->header_data ?? [];
$mergedHeaderData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$mergedHeaderData,
$page
);
}
// Render Footer Template
$renderedFooter = null;
$footerTemplate = $page->footerTemplate;
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
if (!$footerTemplate) {
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
if ($footerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $page->footer_data ?? [];
$mergedFooterData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$page
);
}
return view($view, [
'page' => $page,
'settings' => $settings,
'sections' => $sections,
'templatedSections' => $templatedSections,
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => [
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $settings->default_meta_image ?? null,
],
]);
return $this->renderPage($page, $settings);
}
/**
* Display a specific page by slug
*/
public function show($slug)
public function show($slug)
{
return $this->handleShow($slug);
}
/**
* Handle nested page routes (e.g. /kurumsal/hakkimizda)
*/
public function showNested($parentSlug, $slug)
{
return $this->handleShow($slug);
}
/**
* Handle akademi.truncgil.com.tr subdomain
*/
public function showAkademi()
{
return $this->handleShow('truncgil-akademi');
}
protected function handleShow($slug)
{
$settings = $this->getSettings();
if ($slug === 'akademi') {
$slug = 'truncgil-akademi';
}
// Redirection logic disabled for rendering the page locally on main domain.
// 1. Statik View Kontrolü
if (view()->exists("templates.$slug")) {
$page = Page::with(['headerTemplate', 'footerTemplate'])
->where('slug', $slug)
->where('status', 'published')
->first();
return $this->renderView("templates.$slug", $page, $settings);
}
// 2. Normal Dinamik Akış (DB Kaydı Zorunlu)
$page = Page::with(['headerTemplate', 'footerTemplate'])
->where('slug', $slug)
->where('status', 'published')
->firstOrFail();
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
return $this->renderPage($page, $settings);
}
$metaTitle = method_exists($page, 'translate')
? ($page->translate('meta_title') ?: $page->translate('title'))
: ($page->meta_title ?? $page->title ?? null);
$metaDescription = method_exists($page, 'translate')
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
: ($page->meta_description ?? $page->excerpt ?? null);
// Use parsed_sections for easier template usage (key-value format)
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? [];
// Yeni dinamik template sistemi için sections_data varsa onu da al
$templatedSections = $page->templated_sections ?? collect([]);
// Template belirleme mantığı:
// 1. Kullanıcı template seçmişse, onu kullan
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
// 3. Hiçbiri yoksa, 'generic' kullan
/**
* Render a page with full dynamic logic
*/
protected function renderPage(Page $page, $settings)
{
$template = $page->template
?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
?? (($page->is_homepage ?? false) ? 'home' : 'generic');
$view = view()->exists("templates.$template")
? "templates.$template"
: 'templates.generic';
return $this->renderView($view, $page, $settings);
}
// Render Header Template
/**
* Shared rendering logic for both static and dynamic templates
*/
protected function renderView(string $view, ?Page $page, $settings)
{
$renderedHeader = null;
$renderedFooter = null;
if ($page) {
$renderedHeader = $this->renderHeader($page, $settings);
$renderedFooter = $this->renderFooter($page, $settings);
}
$meta = $this->getMeta($page, $settings);
$pageUrl = url()->current();
return view($view, [
'page' => $page,
'pageHero' => $page ? PageTemplateHero::resolveForPage($page) : null,
'settings' => $settings,
'sections' => $page ? ($page->parsed_sections ?? $page->sections ?? $page->data ?? []) : [],
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => $meta,
'structuredData' => PageStructuredData::forPage(
$page,
$pageUrl,
$meta['title'] ?? null,
$meta['description'] ?? null,
),
]);
}
/**
* Get settings as an object
*/
protected function getSettings()
{
if (!class_exists(Setting::class)) {
return (object) [];
}
return (object) Setting::where('is_active', true)
->pluck('value', 'key')
->toArray();
}
/**
* Get meta information for a page
*/
protected function getMeta(?Page $page, $settings)
{
$metaTitle = null;
$metaDescription = null;
$metaImage = $settings->default_meta_image ?? null;
if ($page) {
$metaTitle = method_exists($page, 'translate')
? ($page->translate('meta_title') ?: $page->translate('title'))
: ($page->meta_title ?? $page->title ?? null);
$metaDescription = method_exists($page, 'translate')
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
: ($page->meta_description ?? $page->excerpt ?? null);
if ($page->featured_image_url) {
$metaImage = $page->featured_image_url;
}
}
return [
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $metaImage,
];
}
/**
* Render header template
*/
protected function renderHeader(Page $page, $settings)
{
// 1. Önce özel blade dosyası var mı kontrol et
if (!empty($page->custom_header_blade) && view()->exists($page->custom_header_blade)) {
return view($page->custom_header_blade, ['page' => $page])->render();
}
// 2. Yoksa Header Template sistemini kullan
$headerTemplate = $page->headerTemplate;
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
if (!$headerTemplate) {
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
if (!$headerTemplate && isset($settings->default_header)) {
$headerTemplate = HeaderTemplate::find($settings->default_header);
}
if ($headerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $page->header_data ?? [];
$mergedHeaderData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders(
return TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$mergedHeaderData,
$page
);
}
// Render Footer Template
$renderedFooter = null;
return null;
}
/**
* Render footer template
*/
protected function renderFooter(Page $page, $settings)
{
// 1. Önce özel blade dosyası var mı kontrol et
if (!empty($page->custom_footer_blade) && view()->exists($page->custom_footer_blade)) {
return view($page->custom_footer_blade, ['page' => $page])->render();
}
// 2. Yoksa Footer Template sistemini kullan
$footerTemplate = $page->footerTemplate;
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
if (!$footerTemplate) {
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
if (!$footerTemplate && isset($settings->default_footer)) {
$footerTemplate = FooterTemplate::find($settings->default_footer);
}
if ($footerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $page->footer_data ?? [];
$mergedFooterData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders(
return TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$page
);
}
return view($view, [
'page' => $page,
'settings' => $settings,
'sections' => $sections,
'templatedSections' => $templatedSections,
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => [
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $settings->default_meta_image ?? null,
],
]);
return null;
}
/**
@@ -248,6 +276,3 @@ public function show($slug)
->get();
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
namespace App\Http\Controllers;
use App\Models\Page;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class PaymentController extends Controller
{
public function process(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'surname' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'required|string|max:20',
'price' => 'required|numeric|min:1',
'address' => 'nullable|string',
'agreement' => 'accepted',
], [
'name.required' => 'Lütfen adınızı giriniz.',
'surname.required' => 'Lütfen soyadınızı giriniz.',
'email.required' => 'Lütfen e-posta adresinizi giriniz.',
'email.email' => 'Geçerli bir e-posta adresi giriniz.',
'phone.required' => 'Lütfen telefon numaranızı giriniz.',
'price.required' => 'Lütfen ödeme tutarını giriniz.',
'price.min' => 'Ödeme tutarı en az 1 TL olmalıdır.',
'address.required' => 'Lütfen ödeme açıklamasını giriniz.',
'agreement.accepted' => 'Lütfen sözleşmeyi onaylayınız.',
]);
if ($validator->fails()) {
Log::info('Payment Validation Failed', $validator->errors()->toArray());
return back()->withErrors($validator)->withInput();
}
$merchant_id = '127117';
$merchant_key = 's9GH5FMuxFtqjxyJ';
$merchant_salt = 'qTHTx3j4FjieXPP3';
$price = $request->input('price') * 100; // Krş cinsinden
// Get user IP (handling Cloudflare or standard)
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
$user_ip = $ip;
$merchant_oid = time(); // Benzersiz sipariş no
$email = $request->input('email');
$payment_amount = $price;
$currency = 'TL'; // Default
$test_mode = 0; // Canlı mod
$user_name = $request->input('name').' '.$request->input('surname');
$user_address = $request->input('address') ?: 'Adres Belirtilmemiş'; // Fallback if not provided
$user_phone = $request->input('phone');
// Callbacks should ideally be routes we define, but user provided specific URLs.
// We will use routes relative to current domain or the ones user specified if they are absolute?
// The user script had: "https://www.truncgil.com.tr/pay.php?ok"
// I should probably use the current app's URL.
$merchant_ok_url = route('payment.success');
$merchant_fail_url = route('payment.fail');
$user_basket = base64_encode(json_encode([
['Web / Mobil Yazılım', $payment_amount, 1],
]));
$no_installment = 0;
$max_installment = 9;
$debug_on = 1;
$timeout_limit = 30;
$hash_str = $merchant_id.$user_ip.$merchant_oid.$email.$payment_amount.$user_basket.$no_installment.$max_installment.$currency.$test_mode;
$paytr_token = base64_encode(hash_hmac('sha256', $hash_str.$merchant_salt, $merchant_key, true));
$post_vals = [
'merchant_id' => $merchant_id,
'user_ip' => $user_ip,
'merchant_oid' => $merchant_oid,
'email' => $email,
'lang' => 'tr',
'payment_amount' => $payment_amount,
'paytr_token' => $paytr_token,
'user_basket' => $user_basket,
'debug_on' => $debug_on,
'no_installment' => $no_installment,
'max_installment' => $max_installment,
'user_name' => $user_name,
'user_address' => $user_address,
'user_phone' => $user_phone,
'merchant_ok_url' => $merchant_ok_url,
'merchant_fail_url' => $merchant_fail_url,
'timeout_limit' => $timeout_limit,
'currency' => $currency,
'test_mode' => $test_mode,
];
// Log request for debugging
Log::info('PayTR Request', [
'merchant_id' => $merchant_id,
'merchant_oid' => $merchant_oid,
'email' => $email,
'payment_amount' => $payment_amount,
'user_ip' => $user_ip,
]);
// Curl request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.paytr.com/odeme/api/get-token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_vals);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$result = curl_exec($ch);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
curl_close($ch);
if ($curlErrno) {
Log::error('PayTR cURL error', ['errno' => $curlErrno, 'error' => $curlError]);
return back()->with('error', 'PAYTR bağlantı hatası. Lütfen tekrar deneyin.');
}
if ($result === false || empty($result)) {
Log::error('PayTR returned empty response');
return back()->with('error', 'Ödeme sisteminden yanıt alınamadı. Lütfen tekrar deneyin.');
}
// Log the raw response for debugging
Log::info('PayTR Raw Response', ['response' => $result]);
$result = json_decode($result, 1);
// Check if result is valid
if (!$result || !is_array($result)) {
Log::error('PayTR API returned invalid response', ['response' => $result]);
return back()->with('error', 'Ödeme sisteminden geçersiz yanıt alındı. Lütfen tekrar deneyin.');
}
if (!isset($result['status'])) {
Log::error('PayTR API response missing status', ['response' => $result]);
return back()->with('error', 'Ödeme sisteminden beklenmeyen yanıt alındı. Lütfen tekrar deneyin.');
}
if ($result['status'] == 'success') {
$token = $result['token'];
// Re-render the online-payment page but with the token
// We need to fetch the page content again
// Assuming the slug is 'online-odeme' or similar, or we can just pass a dummy page object
// if we are lazy, but correct way is to fetch the page associated with the route.
// Since we are POSTing here, we don't know the Page model context unless we fetch it.
// Let's try to assume 'online-odeme' which is common.
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if (! $page) {
$page = new Page(['title' => 'Online Ödeme', 'content' => '', 'excerpt' => 'Please complete your payment below.']);
}
return view('templates.corporate.online-payment', [
'page' => $page,
'paytr_token' => $token,
]);
} else {
return back()->with('error', 'PAYTR failed. reason:'.$result['reason']);
}
}
public function success(Request $request)
{
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if ($page) {
return redirect()->route('page.show', $page->slug)->with('success', 'Your payment has been charged successfully. This will appear on your credit card statement as "PAYTR ODEME HIZMETLERI"');
}
return redirect()->route('home')->with('success', 'Your payment has been charged successfully.');
}
public function fail(Request $request)
{
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if ($page) {
return redirect()->route('page.show', $page->slug)->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
}
return redirect()->route('home')->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
}
}
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Setting;
use App\Models\HeaderTemplate;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function show($slug)
{
$product = Product::where('slug', $slug)
->where('is_active', true)
->with('category')
->firstOrFail();
// Get Settings
$settings = new \stdClass();
$allSettings = Setting::where('is_active', true)->get();
foreach ($allSettings as $setting) {
$settings->{$setting->key} = $setting->value;
}
// --- Header Logic ---
$renderedHeader = null;
$defaultHeaderId = $settings->default_header ?? null;
if ($defaultHeaderId) {
$headerTemplate = HeaderTemplate::find($defaultHeaderId);
if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? [];
$productData = []; // Ürünlere özel header datası eklenebilir
$mergedHeaderData = array_merge($templateDefaults, $productData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$mergedHeaderData,
$product // Model context
);
}
}
// --- Footer Logic ---
$renderedFooter = null;
$defaultFooterId = $settings->default_footer ?? null;
if ($defaultFooterId) {
$footerTemplate = FooterTemplate::find($defaultFooterId);
if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? [];
$productData = []; // Ürünlere özel footer datası eklenebilir
$mergedFooterData = array_merge($templateDefaults, $productData);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$product // Model context
);
}
}
$meta = [
'title' => $product->translate('title'),
'description' => \Str::limit(strip_tags($product->translate('content')), 160),
'image' => $product->hero_image ? asset('storage/' . $product->hero_image) : null,
];
// Use custom view template if specified
if ($product->view_template && view()->exists($product->view_template)) {
return view($product->view_template, compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
}
// Use landing page template if landing_page_data exists
if (!empty($product->landing_page_data)) {
return view('front.products.landing', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
}
// Default view
return view('front.products.show', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Proposal;
use Illuminate\Http\Request;
class ProposalController extends Controller
{
/**
* Display the public proposal presentation page.
*/
public function show(string $slug)
{
$proposal = Proposal::where('slug', $slug)->firstOrFail();
// Increment views count cleanly
$proposal->increment('views_count');
return view('proposals.show', compact('proposal'));
}
/**
* Handle client interaction actions (approve or revision/feedback).
*/
public function action(Request $request, string $slug)
{
$proposal = Proposal::where('slug', $slug)->firstOrFail();
$actionType = $request->input('action_type');
if ($actionType === 'approve') {
$request->validate([
'name' => 'required|string|max:255',
]);
$proposal->update([
'status' => 'accepted',
'accepted_name' => $request->input('name'),
'accepted_at' => now(),
]);
return response()->json([
'success' => true,
'message' => 'Teklif başarıyla onaylandı! Katılımınız ve iş birliğiniz için teşekkür ederiz.',
]);
}
if ($actionType === 'feedback') {
$request->validate([
'feedback' => 'required|string|max:5000',
]);
$proposal->update([
'status' => 'revised',
'client_feedback' => $request->input('feedback'),
]);
return response()->json([
'success' => true,
'message' => 'Revizyon ve görüşleriniz başarıyla iletildi. En kısa sürede güncellenerek sizinle paylaşılacaktır.',
]);
}
return response()->json([
'success' => false,
'message' => 'Geçersiz işlem tipi.',
], 400);
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Page;
use App\Models\Blog;
use App\Models\Product;
use Illuminate\Http\Response;
class SitemapController extends Controller
{
public function index(): Response
{
$urls = [];
// 1. Static & Main Pages
$urls[] = [
'loc' => url('/'),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'daily',
'priority' => '1.0',
];
$urls[] = [
'loc' => route('blog.index'),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.8',
];
$urls[] = [
'loc' => route('career.index'),
'lastmod' => now()->startOfMonth()->toAtomString(),
'changefreq' => 'monthly',
'priority' => '0.5',
];
// 2. Dynamic Pages
$pages = Page::where('status', 'published')
->where('is_homepage', false)
->get();
foreach ($pages as $page) {
$urls[] = [
'loc' => url($page->slug),
'lastmod' => $page->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
];
}
// 3. Blog Posts
if (class_exists(Blog::class)) {
$posts = Blog::published()->get();
foreach ($posts as $post) {
$urls[] = [
'loc' => route('blog.show', $post->slug),
'lastmod' => $post->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.6',
];
}
}
// 4. Products
if (class_exists(Product::class)) {
$products = Product::where('is_active', true)->get();
foreach ($products as $product) {
$urls[] = [
'loc' => route('products.show', $product->slug),
'lastmod' => $product->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
];
}
}
return response()->view('sitemap', compact('urls'))
->header('Content-Type', 'text/xml');
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureSecurityHeaders
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// HSTS (Strict-Transport-Security) - Enforce HTTPS for 1 year
// 'includeSubDomains' and 'preload' are optional but often recommended for full security.
// A+ score typically looks for this header with a long max-age.
if ($request->isSecure()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
// X-Frame-Options: SAMEORIGIN (prevents clickjacking by allowing framing only by the same site)
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// X-Content-Type-Options: nosniff (prevents MIME type sniffing)
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Referrer-Policy: stringent check, usually 'strict-origin-when-cross-origin' is good
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// X-XSS-Protection: 1; mode=block (deprecated but still good for older browsers, some scanners check it)
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Permissions-Policy: restrict dangerous features
// This is a bit strict, might need adjustment based on site features.
// For general sites: geolocation=(), microphone=(), camera=() is often safe.
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Cross-Origin-Opener-Policy: isolate browsing context from cross-origin documents
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
// Content-Security-Policy (CSP) - Start with upgrade-insecure-requests to force HTTPS assets
// A full CSP can be tricky and break scripts. 'upgrade-insecure-requests' is safe and good for mixed content.
// We can add 'frame-ancestors' here too if X-Frame-Options is ignored by some.
// If the user wants A+, a basic CSP is often enough.
$csp = "upgrade-insecure-requests; block-all-mixed-content; frame-ancestors 'self';";
$response->headers->set('Content-Security-Policy', $csp);
return $response;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SuperAdminMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Unauthorized'], 403);
}
abort(403);
}
return $next($request);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use Illuminate\Database\Eloquent\Model;
class InternJournalTimeline extends Component
{
public ?Model $record = null;
public function getGithubRepoProperty(): ?string
{
return $this->record?->github_repo;
}
public function render()
{
return view('livewire.intern-journal-timeline');
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Award extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
protected $fillable = [
'title',
'description',
'issuer',
'award_date',
'image',
'external_link',
'is_featured',
'is_active',
'sort_order',
'category',
];
/**
* @var list<string>
*/
protected $translatable = [
'title',
'description',
'issuer',
];
protected $casts = [
'award_date' => 'date',
'is_featured' => 'boolean',
'is_active' => 'boolean',
'sort_order' => 'integer',
];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
public function scopeOrdered($query)
{
return $query
->orderBy('sort_order', 'asc')
->orderBy('award_date', 'desc');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BlogTopic extends Model
{
use HasFactory;
protected $fillable = [
'name',
'context',
'status',
'last_used_at',
];
protected $casts = [
'last_used_at' => 'datetime',
];
public function scopeActive($query)
{
return $query->where('status', 'active');
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CareerApplication extends Model
{
use HasFactory;
protected $fillable = [
'name',
'email',
'phone',
'type',
'cv_path',
'nda_path',
'contract_path',
'id_photocopy_path',
'git_knowledge',
'ai_usage',
'message',
'status',
'username',
'password',
'signed_internship_form_path',
'to_be_signed_internship_form_path',
'internship_start_date',
'internship_end_date',
'internship_total_days',
'github_repo',
'github_username',
'certificate_code',
'transcript_markdown',
'notebook_supervisor_signed',
'notebook_supervisor_name',
'notebook_unit_signed',
'notebook_unit_name',
'notebook_approved',
];
/**
* Get the journal entries for the internship application.
*/
public function journalEntries()
{
return $this->hasMany(InternshipJournalEntry::class);
}
protected static function booted()
{
static::saving(function ($model) {
if ($model->type === 'internship') {
$model->username = $model->email;
if (!$model->certificate_code) {
do {
$code = 'TRN-' . date('Y') . '-' . strtoupper(\Illuminate\Support\Str::random(4)) . '-' . strtoupper(\Illuminate\Support\Str::random(4));
} while (static::where('certificate_code', $code)->exists());
$model->certificate_code = $code;
}
}
});
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'password' => 'hashed',
'notebook_supervisor_signed' => 'boolean',
'notebook_unit_signed' => 'boolean',
'notebook_approved' => 'boolean',
];
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace App\Models;
use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class CompanyHistoryItem extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
public const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'];
protected $fillable = [
'year',
'quarter',
'title',
'content',
'color',
'icon',
'position',
'is_active',
'sort_order',
];
/**
* @var list<string>
*/
protected $translatable = [
'title',
'content',
];
protected $casts = [
'year' => 'integer',
'is_active' => 'boolean',
'sort_order' => 'integer',
];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeOrdered($query)
{
return $query
->orderBy('year', 'asc')
->orderByRaw("CASE quarter WHEN 'Q1' THEN 1 WHEN 'Q2' THEN 2 WHEN 'Q3' THEN 3 WHEN 'Q4' THEN 4 ELSE 5 END")
->orderBy('sort_order', 'asc');
}
public function getResolvedPositionAttribute(): string
{
if (in_array($this->position, ['left', 'right'], true)) {
return $this->position;
}
return ($this->sort_order % 2 === 1) ? 'right' : 'left';
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class InternshipJournalEntry extends Model
{
use HasFactory;
protected $fillable = [
'career_application_id',
'day_number',
'date',
'content',
'is_retroactive',
'supervisor_approved',
'unit_approved',
'supervisor_name',
];
protected $casts = [
'is_retroactive' => 'boolean',
'supervisor_approved' => 'boolean',
'unit_approved' => 'boolean',
];
/**
* Get the career application that owns this journal entry.
*/
public function careerApplication()
{
return $this->belongsTo(CareerApplication::class);
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace App\Models;
use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class MusicProduction extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
protected $table = 'music_productions';
protected $fillable = [
'title',
'slug',
'spotify_album_id',
'spotify_url',
'spotify_cover_url',
'spotify_type',
'spotify_synced_at',
'youtube_video_id',
'youtube_url',
'youtube_cover_url',
'youtube_description',
'youtube_synced_at',
'cover_image',
'content',
'client_name',
'production_date',
'gallery',
'is_active',
'sort_order',
];
/**
* Translatable fields
*/
protected $translatable = [
'title',
'content',
'client_name',
];
protected $casts = [
'production_date' => 'date',
'spotify_synced_at' => 'datetime',
'youtube_synced_at' => 'datetime',
'gallery' => 'array',
'is_active' => 'boolean',
'sort_order' => 'integer',
];
public function getUrlAttribute()
{
return route('music-productions.show', $this->slug);
}
public function getCoverImageUrlAttribute()
{
if ($this->cover_image) {
return asset('storage/' . $this->cover_image);
}
if ($this->spotify_cover_url) {
return $this->spotify_cover_url;
}
if ($this->youtube_cover_url) {
return $this->youtube_cover_url;
}
return null;
}
public function getDisplayCoverImageAttribute(): ?string
{
if ($this->cover_image) {
return $this->cover_image;
}
return null;
}
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeOrdered($query)
{
return $query->orderBy('sort_order', 'asc')->orderBy('production_date', 'desc');
}
}
+15 -6
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Models\SectionTemplate;
use App\Support\PageTemplateHero;
use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -34,8 +35,10 @@ class Page extends Model
// Template System
'header_template_id',
'header_data',
'custom_header_blade',
'footer_template_id',
'footer_data',
'custom_footer_blade',
'sections_data',
];
@@ -78,6 +81,11 @@ class Page extends Model
return $this->hasMany(Page::class, 'parent_id');
}
public function testimonials()
{
return $this->hasMany(Testimonial::class);
}
/**
* Get the header template for the page.
*/
@@ -108,13 +116,14 @@ class Page extends Model
return '/' . $this->slug;
}
public function getFeaturedImageUrlAttribute()
public function getFeaturedImageUrlAttribute(): ?string
{
if ($this->featured_image) {
return asset('storage/' . $this->featured_image);
}
return null;
return PageTemplateHero::urlForPage($this);
}
public function usesTemplateHeroImage(): bool
{
return ! $this->featured_image && PageTemplateHero::hasHero($this->template);
}
/**
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\HasTranslations;
class Product extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
protected $fillable = [
'title',
'slug',
'type',
'product_category_id',
'hero_image',
'content',
'view_template',
'external_url',
'sort_order',
'is_active',
'landing_page_data',
];
protected $translatable = [
'title',
'content',
];
protected $casts = [
'is_active' => 'boolean',
'title' => 'array',
'content' => 'array',
'landing_page_data' => 'array',
];
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
if (empty($model->title)) {
$translations = request()->input('translations');
$defaultLocale = app()->getLocale();
if (is_array($translations)) {
if (!empty($translations[$defaultLocale]['title'])) {
$model->title = $translations[$defaultLocale]['title'];
} else {
foreach ($translations as $locale => $fields) {
if (!empty($fields['title'])) {
$model->title = $fields['title'];
break;
}
}
}
}
if (empty($model->title)) {
$model->title = [];
}
}
});
}
public function category()
{
return $this->belongsTo(ProductCategory::class, 'product_category_id');
}
}

Some files were not shown because too many files have changed in this diff Show More