├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── README.md ├── Screenshots ├── Showcase-1.gif └── screen-1.png ├── app ├── Console │ └── Commands │ │ └── GenerateUUIDs.php ├── Helpers │ └── general.php ├── Http │ ├── Controllers │ │ ├── Admin │ │ │ ├── AdminController.php │ │ │ └── GitHubController.php │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── CommentController.php │ │ ├── Controller.php │ │ ├── CreativeController.php │ │ ├── ItemController.php │ │ ├── PagesController.php │ │ ├── ProfileController.php │ │ └── ProjectController.php │ ├── Middleware │ │ └── Admin.php │ └── Requests │ │ ├── Auth │ │ └── LoginRequest.php │ │ └── ProfileUpdateRequest.php ├── Jobs │ ├── ItemNewStatusNotification.php │ └── SendNewCommentNotification.php ├── Livewire │ └── ItemStatusSelector.php ├── Mail │ ├── ItemNewStatus.php │ └── NewItemComment.php ├── Models │ ├── Comment.php │ ├── Creative.php │ ├── Destination.php │ ├── Item.php │ ├── Project.php │ └── User.php ├── Policies │ └── ItemPolicy.php ├── Providers │ └── AppServiceProvider.php ├── Services │ ├── ChatGPTService.php │ ├── ItemUpvoteService.php │ └── QueueWorker.php └── View │ └── Components │ ├── AppLayout.php │ ├── GuestLayout.php │ └── Head │ └── tinymceConfig.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── filesystems.php ├── livewire.php ├── logging.php ├── mail.php ├── openai.php ├── queue.php ├── services.php └── session.php ├── database ├── .gitignore ├── factories │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2025_01_28_155420_create_projects_table.php │ ├── 2025_01_28_155641_create_items_table.php │ ├── 2025_01_28_193840_add_template_to_projects_table.php │ ├── 2025_01_28_215636_create_comments_table.php │ ├── 2025_02_06_223953_add_translated_column_to_items_table.php │ ├── 2025_02_12_092747_change_column_in_items_table.php │ ├── 2025_02_14_200600_add_uuid_to_items_table.php │ ├── 2025_02_22_111529_create_destinations_table.php │ ├── 2025_03_08_212040_create_creatives_table.php │ └── 2025_03_20_170310_add_github_to_users_table.php └── seeders │ └── DatabaseSeeder.php ├── lang ├── de │ ├── auth.php │ ├── items.php │ ├── pagination.php │ ├── passwords.php │ ├── profile.php │ ├── projects.php │ └── validation.php └── en │ ├── auth.php │ ├── items.php │ ├── pagination.php │ ├── passwords.php │ ├── profile.php │ ├── projects.php │ └── validation.php ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── assets │ └── dropzone-5.9.3 │ │ ├── dropzone.min.css │ │ └── dropzone.min.js ├── favicon.png ├── images │ └── project-default.png ├── index.php ├── js │ └── tinymce │ │ ├── .npmignore │ │ ├── README.md │ │ ├── bower.json │ │ ├── composer.json │ │ ├── icons │ │ └── default │ │ │ ├── icons.js │ │ │ ├── icons.min.js │ │ │ └── index.js │ │ ├── license.md │ │ ├── models │ │ └── dom │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ └── model.min.js │ │ ├── package.json │ │ ├── plugins │ │ ├── accordion │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── advlist │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── anchor │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── autolink │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── autoresize │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── autosave │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── charmap │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── code │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── codesample │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── directionality │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── emoticons │ │ │ ├── index.js │ │ │ ├── js │ │ │ │ ├── emojiimages.js │ │ │ │ ├── emojiimages.min.js │ │ │ │ ├── emojis.js │ │ │ │ └── emojis.min.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── fullscreen │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── help │ │ │ ├── index.js │ │ │ ├── js │ │ │ │ └── i18n │ │ │ │ │ └── keynav │ │ │ │ │ ├── ar.js │ │ │ │ │ ├── bg_BG.js │ │ │ │ │ ├── ca.js │ │ │ │ │ ├── cs.js │ │ │ │ │ ├── da.js │ │ │ │ │ ├── de.js │ │ │ │ │ ├── el.js │ │ │ │ │ ├── en.js │ │ │ │ │ ├── es.js │ │ │ │ │ ├── eu.js │ │ │ │ │ ├── fa.js │ │ │ │ │ ├── fi.js │ │ │ │ │ ├── fr_FR.js │ │ │ │ │ ├── he_IL.js │ │ │ │ │ ├── hi.js │ │ │ │ │ ├── hr.js │ │ │ │ │ ├── hu_HU.js │ │ │ │ │ ├── id.js │ │ │ │ │ ├── it.js │ │ │ │ │ ├── ja.js │ │ │ │ │ ├── kk.js │ │ │ │ │ ├── ko_KR.js │ │ │ │ │ ├── ms.js │ │ │ │ │ ├── nb_NO.js │ │ │ │ │ ├── nl.js │ │ │ │ │ ├── pl.js │ │ │ │ │ ├── pt_BR.js │ │ │ │ │ ├── pt_PT.js │ │ │ │ │ ├── ro.js │ │ │ │ │ ├── ru.js │ │ │ │ │ ├── sk.js │ │ │ │ │ ├── sl_SI.js │ │ │ │ │ ├── sv_SE.js │ │ │ │ │ ├── th_TH.js │ │ │ │ │ ├── tr.js │ │ │ │ │ ├── uk.js │ │ │ │ │ ├── vi.js │ │ │ │ │ ├── zh_CN.js │ │ │ │ │ └── zh_TW.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── image │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── importcss │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── insertdatetime │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── link │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── lists │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── media │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── nonbreaking │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── pagebreak │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── preview │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── quickbars │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── save │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── searchreplace │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── table │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── visualblocks │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── visualchars │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ └── wordcount │ │ │ ├── index.js │ │ │ ├── plugin.js │ │ │ └── plugin.min.js │ │ ├── skins │ │ ├── content │ │ │ ├── dark │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── default │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── document │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── tinymce-5-dark │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ ├── tinymce-5 │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ │ └── writer │ │ │ │ ├── content.css │ │ │ │ ├── content.js │ │ │ │ └── content.min.css │ │ └── ui │ │ │ ├── oxide-dark │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.js │ │ │ ├── content.inline.min.css │ │ │ ├── content.js │ │ │ ├── content.min.css │ │ │ ├── skin.css │ │ │ ├── skin.js │ │ │ ├── skin.min.css │ │ │ ├── skin.shadowdom.css │ │ │ ├── skin.shadowdom.js │ │ │ └── skin.shadowdom.min.css │ │ │ ├── oxide │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.js │ │ │ ├── content.inline.min.css │ │ │ ├── content.js │ │ │ ├── content.min.css │ │ │ ├── skin.css │ │ │ ├── skin.js │ │ │ ├── skin.min.css │ │ │ ├── skin.shadowdom.css │ │ │ ├── skin.shadowdom.js │ │ │ └── skin.shadowdom.min.css │ │ │ ├── tinymce-5-dark │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.js │ │ │ ├── content.inline.min.css │ │ │ ├── content.js │ │ │ ├── content.min.css │ │ │ ├── skin.css │ │ │ ├── skin.js │ │ │ ├── skin.min.css │ │ │ ├── skin.shadowdom.css │ │ │ ├── skin.shadowdom.js │ │ │ └── skin.shadowdom.min.css │ │ │ └── tinymce-5 │ │ │ ├── content.css │ │ │ ├── content.inline.css │ │ │ ├── content.inline.js │ │ │ ├── content.inline.min.css │ │ │ ├── content.js │ │ │ ├── content.min.css │ │ │ ├── skin.css │ │ │ ├── skin.js │ │ │ ├── skin.min.css │ │ │ ├── skin.shadowdom.css │ │ │ ├── skin.shadowdom.js │ │ │ └── skin.shadowdom.min.css │ │ ├── themes │ │ └── silver │ │ │ ├── index.js │ │ │ ├── theme.js │ │ │ └── theme.min.js │ │ ├── tinymce.d.ts │ │ ├── tinymce.js │ │ └── tinymce.min.js └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── app.js │ └── bootstrap.js └── views │ ├── admin │ └── index.blade.php │ ├── auth │ ├── confirm-password.blade.php │ ├── forgot-password.blade.php │ ├── login.blade.php │ ├── register.blade.php │ ├── reset-password.blade.php │ └── verify-email.blade.php │ ├── components │ ├── auth-session-status.blade.php │ ├── author-info.blade.php │ ├── box.blade.php │ ├── checkbox-group.blade.php │ ├── danger-button.blade.php │ ├── dropdown-link.blade.php │ ├── dropdown.blade.php │ ├── forms │ │ └── tinymce-editor.blade.php │ ├── h1.blade.php │ ├── h2.blade.php │ ├── head │ │ └── tinymce-config.blade.php │ ├── icons │ │ ├── app.blade.php │ │ ├── comments.blade.php │ │ ├── delete.blade.php │ │ ├── edit.blade.php │ │ ├── items.blade.php │ │ ├── markdown.blade.php │ │ ├── right-arrow.blade.php │ │ ├── up-arrow-green.blade.php │ │ └── up-arrow.blade.php │ ├── input-error.blade.php │ ├── input-group.blade.php │ ├── input-label.blade.php │ ├── item-priority.blade.php │ ├── item-table.blade.php │ ├── item-type.blade.php │ ├── modal.blade.php │ ├── nav-link.blade.php │ ├── primary-button.blade.php │ ├── primary-link-button.blade.php │ ├── responsive-nav-link.blade.php │ ├── secondary-button.blade.php │ ├── select-group.blade.php │ ├── text-input.blade.php │ ├── textarea-group.blade.php │ └── textarea.blade.php │ ├── emails │ ├── item-new-comment.blade.php │ └── item-new-status.blade.php │ ├── errors │ ├── 401.blade.php │ ├── 402.blade.php │ ├── 403.blade.php │ ├── 404.blade.php │ ├── 419.blade.php │ ├── 429.blade.php │ ├── 500.blade.php │ ├── 503.blade.php │ ├── layout.blade.php │ └── minimal.blade.php │ ├── items │ ├── form.blade.php │ └── show.blade.php │ ├── layouts │ ├── app.blade.php │ └── guest.blade.php │ ├── pages │ └── changelog.blade.php │ ├── profile │ ├── edit.blade.php │ └── partials │ │ ├── delete-user-form.blade.php │ │ ├── update-password-form.blade.php │ │ └── update-profile-information-form.blade.php │ └── projects │ ├── form.blade.php │ ├── index.blade.php │ └── show.blade.php ├── routes ├── auth.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ ├── private │ │ └── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ ├── PasswordUpdateTest.php │ │ └── RegistrationTest.php │ ├── ExampleTest.php │ └── ProfileTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=infiniteloop 2 | APP_ENV=local 3 | APP_KEY=base64:v7bkmKOwXlSAmzDhJx3ecmN3yT+IStJL+yrs8aYbBas= 4 | APP_DEBUG=true 5 | APP_TIMEZONE='Europe/Berlin' 6 | APP_URL=https://infiniteloop.test 7 | VITE_APP_NAME="${APP_NAME}" 8 | 9 | APP_LOCALE=en 10 | APP_FALLBACK_LOCALE=en 11 | APP_FAKER_LOCALE=en_EN 12 | 13 | OPENAI_API_KEY='' 14 | OPENAI_ORGANIZATION='' 15 | 16 | APP_MAINTENANCE_DRIVER=file 17 | # APP_MAINTENANCE_STORE=database 18 | 19 | ALLOWED_REGISTRATION_DOMAIN='@domain.tld' 20 | 21 | PHP_CLI_SERVER_WORKERS=4 22 | 23 | BCRYPT_ROUNDS=12 24 | 25 | LOG_CHANNEL=stack 26 | LOG_STACK=single 27 | LOG_DEPRECATIONS_CHANNEL=null 28 | LOG_LEVEL=debug 29 | 30 | DB_CONNECTION=sqlite 31 | # DB_HOST=127.0.0.1 32 | # DB_PORT=3306 33 | # DB_DATABASE=laravel 34 | # DB_USERNAME=root 35 | # DB_PASSWORD= 36 | 37 | SESSION_DRIVER=database 38 | SESSION_LIFETIME=120 39 | SESSION_ENCRYPT=false 40 | SESSION_PATH=/ 41 | SESSION_DOMAIN=null 42 | 43 | BROADCAST_CONNECTION=log 44 | FILESYSTEM_DISK=local 45 | QUEUE_CONNECTION=database 46 | 47 | CACHE_STORE=database 48 | CACHE_PREFIX= 49 | 50 | MEMCACHED_HOST=127.0.0.1 51 | 52 | MAIL_MAILER=log 53 | MAIL_SCHEME=null 54 | MAIL_HOST=127.0.0.1 55 | MAIL_PORT=2525 56 | MAIL_USERNAME=null 57 | MAIL_PASSWORD=null 58 | MAIL_FROM_ADDRESS="hello@example.com" 59 | MAIL_FROM_NAME="${APP_NAME}" 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | npm-debug.log 17 | yarn-error.log 18 | /auth.json 19 | /.fleet 20 | /.idea 21 | /.nova 22 | /.vscode 23 | /.zed 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # infiniteloop 2 | 3 | Use this app to gather UserStories from inside your company. Let users upvote ideas and categories ideas by type and prio. 4 | 5 | ![image](https://raw.githubusercontent.com/1aWebmarketing/infiniteloop/refs/heads/main/Screenshots/Showcase-1.gif) 6 | 7 | ## What's inside? 8 | 9 | - Create, Show, Update and Delete Projects 10 | - Configure a base User Story for every project 11 | - CRUD User Stories 12 | - Set User Story type and priority 13 | - Restrict access to @domain.tld users in .env 14 | - Use /ai in comment to let chatGPT directly change/enrich User Story 15 | 16 | ## Registration Domain Control 17 | 18 | Define your companies Email domain in your `.env` file to prevent external users to register to the platform. 19 | 20 | ## Installation 21 | 22 | Checkout the repository and rename the `.env.example` to `.env`. 23 | 24 | - Run `composer install` 25 | - Run `npm install` 26 | - Run `php artisan migrate` or `php artisan migrate:fresh --seed` 27 | 28 | Missing parameters are marked with `*****` 29 | 30 | ### Naming Conventions 31 | 32 | - Use camelCase for naming throughout the Laravel project. 33 | - In `resources/views/{folders}`, use plural names when working with models like `Companies` or `Orders`. For example, the Media Library should remain singular as it represents a collection of media. 34 | - Route names should also be plural for resources, e.g., `companies.index` or `companies.show`. For the company selector page, `company.select` is used. 35 | - Admin routes use the `admin.` prefix. 36 | 37 | ### Change Documentation 38 | 39 | All contributors are required to document their changes in the `CHANGELOG.md` file. Each entry should include: 40 | - The version number being updated. 41 | - A brief description of the change (e.g., added features, bug fixes, or breaking changes). 42 | - The author or contributor's name (optional). 43 | 44 | This ensures transparency and provides a clear history of the project's evolution. For guidance, follow the existing structure in the `CHANGELOG.md` file. 45 | 46 | ## Security Vulnerabilities 47 | 48 | If you discover any security vulnerabilities within adfinity, feel free to PR or drop an email to infiniteloop@0x25.de. 49 | 50 | ## License 51 | 52 | infiniteloop is open-source :-) 53 | -------------------------------------------------------------------------------- /Screenshots/Showcase-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1aWebmarketing/infiniteloop/a9e316b86b51fbe111f53a84f783792614918fe9/Screenshots/Showcase-1.gif -------------------------------------------------------------------------------- /Screenshots/screen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1aWebmarketing/infiniteloop/a9e316b86b51fbe111f53a84f783792614918fe9/Screenshots/screen-1.png -------------------------------------------------------------------------------- /app/Console/Commands/GenerateUUIDs.php: -------------------------------------------------------------------------------- 1 | each(function ($item) { 31 | $item->uuid = Str::uuid(); 32 | $item->save(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Helpers/general.php: -------------------------------------------------------------------------------- 1 | timezone(config('app.timezone'))->format('d.m.Y H:i'); 11 | } 12 | 13 | function canUpvote(User $user, Item $item) : bool 14 | { 15 | return ItemUpvoteService::canUpvote($user, $item); 16 | } 17 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/AdminController.php: -------------------------------------------------------------------------------- 1 | user()->github_token) 13 | ->get('https://api.github.com/user/repos')->json(); 14 | 15 | $repos = []; 16 | foreach ($response as $repo) { 17 | if('.github' === $repo['name']) continue; 18 | 19 | $repos[$repo['id']] = array( 20 | 'name' => $repo['name'], 21 | 'owner' => $repo['owner']['login'], 22 | ); 23 | } 24 | 25 | return view('admin.index', [ 26 | 'repos' => $repos, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/GitHubController.php: -------------------------------------------------------------------------------- 1 | scopes(['repo', 'admin:org']) 14 | ->redirect(); 15 | } 16 | 17 | public function store() 18 | { 19 | $githubUser = Socialite::driver('github')->user(); 20 | 21 | $admin = auth()->user(); 22 | $admin->github_token = $githubUser->token; 23 | $admin->save(); 24 | 25 | return redirect('/admin')->with('success', 'GitHub connected!'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 28 | 29 | $request->session()->regenerate(); 30 | 31 | return redirect()->intended(route('dashboard', absolute: false)); 32 | } 33 | 34 | /** 35 | * Destroy an authenticated session. 36 | */ 37 | public function destroy(Request $request): RedirectResponse 38 | { 39 | Auth::guard('web')->logout(); 40 | 41 | $request->session()->invalidate(); 42 | 43 | $request->session()->regenerateToken(); 44 | 45 | return redirect('/'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 28 | 'email' => $request->user()->email, 29 | 'password' => $request->password, 30 | ])) { 31 | throw ValidationException::withMessages([ 32 | 'password' => __('auth.password'), 33 | ]); 34 | } 35 | 36 | $request->session()->put('auth.password_confirmed_at', time()); 37 | 38 | return redirect()->intended(route('dashboard', absolute: false)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 17 | return redirect()->intended(route('dashboard', absolute: false)); 18 | } 19 | 20 | $request->user()->sendEmailVerificationNotification(); 21 | 22 | return back()->with('status', 'verification-link-sent'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 18 | ? redirect()->intended(route('dashboard', absolute: false)) 19 | : view('auth.verify-email'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validateWithBag('updatePassword', [ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back()->with('status', 'password-updated'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 29 | 'email' => ['required', 'email'], 30 | ]); 31 | 32 | // We will send the password reset link to this user. Once we have attempted 33 | // to send the link, we will examine the response then see the message we 34 | // need to show to the user. Finally, we'll send out a proper response. 35 | $status = Password::sendResetLink( 36 | $request->only('email') 37 | ); 38 | 39 | return $status == Password::RESET_LINK_SENT 40 | ? back()->with('status', __($status)) 41 | : back()->withInput($request->only('email')) 42 | ->withErrors(['email' => __($status)]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 35 | 'name' => ['required', 'string', 'max:255'], 36 | 'email' => [ 37 | 'required', 38 | 'string', 39 | 'lowercase', 40 | 'email', 41 | 'max:255', 42 | 'unique:' . User::class, 43 | function ($attribute, $value, $fail) use ($allowedDomain) { 44 | if (!str_ends_with($value, $allowedDomain)) { 45 | $fail("The email must belong to the allowed domain: $allowedDomain."); 46 | } 47 | }, 48 | ], 49 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 50 | ]); 51 | 52 | $user = User::create([ 53 | 'name' => $request->name, 54 | 'email' => $request->email, 55 | 'password' => Hash::make($request->password), 56 | ]); 57 | 58 | event(new Registered($user)); 59 | 60 | Auth::login($user); 61 | 62 | return redirect(route('dashboard', absolute: false)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19 | } 20 | 21 | if ($request->user()->markEmailAsVerified()) { 22 | event(new Verified($request->user())); 23 | } 24 | 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/CommentController.php: -------------------------------------------------------------------------------- 1 | validate([ 16 | 'text' => 'required|string', 17 | ]); 18 | 19 | $fields['item_id'] = $item->id; 20 | $fields['user_id'] = auth()->id(); 21 | 22 | $comment = Comment::create($fields); 23 | 24 | SendNewCommentNotification::dispatch($item, $comment); 25 | 26 | if(!str_contains($fields['text'], '/ki') === false){ 27 | $fields['text'] = str_replace('/ki', '', $fields['text']); 28 | ChatGPTService::optimizeItem($item, $fields['text']); 29 | } 30 | 31 | return redirect() 32 | ->route('items.show', [ 33 | 'project' => $item->project->id, 34 | 'item' => $item->uuid 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | validate([ 17 | 'file' => 'required|mimes:jpeg,jpg,png,mp4,mov,webp,webm', 18 | ]); 19 | 20 | if ($request->file('file')->getSize() > 100_000_000) { 21 | return response()->json([ 22 | 'success' => false, 23 | 'error' => 'File is bigger than 100Mb', 24 | ], 400); 25 | } 26 | 27 | $file = $request->file('file'); 28 | 29 | // Determine file type 30 | $type = match (strtolower($file->getClientOriginalExtension())) { 31 | 'mp4', 'mov', 'webm' => 'VIDEO', 32 | default => 'IMAGE', 33 | }; 34 | 35 | // Generate a unique filename 36 | $randomFileName = Str::uuid() . '.' . $file->getClientOriginalExtension(); 37 | 38 | $path = $file->storeAs("creatives", $randomFileName, 'public'); 39 | 40 | Creative::create([ 41 | 'item_id' => $item->id, 42 | 'name' => $file->getClientOriginalName(), 43 | 'type' => $type, 44 | 'path' => $path, 45 | ]); 46 | 47 | return response()->json([ 48 | 'message' => 'File uploaded successfully.', 49 | 'type' => $type, 50 | 'path' => Storage::url($path), 51 | ]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Http/Controllers/PagesController.php: -------------------------------------------------------------------------------- 1 | $request->user(), 21 | ]); 22 | } 23 | 24 | /** 25 | * Update the user's profile information. 26 | */ 27 | public function update(ProfileUpdateRequest $request): RedirectResponse 28 | { 29 | $request->user()->fill($request->validated()); 30 | 31 | if ($request->user()->isDirty('email')) { 32 | $request->user()->email_verified_at = null; 33 | } 34 | 35 | $request->user()->save(); 36 | 37 | return Redirect::route('profile.edit')->with('status', 'profile-updated'); 38 | } 39 | 40 | /** 41 | * Delete the user's account. 42 | */ 43 | public function destroy(Request $request): RedirectResponse 44 | { 45 | $request->validateWithBag('userDeletion', [ 46 | 'password' => ['required', 'current_password'], 47 | ]); 48 | 49 | $user = $request->user(); 50 | 51 | Auth::logout(); 52 | 53 | $user->delete(); 54 | 55 | $request->session()->invalidate(); 56 | $request->session()->regenerateToken(); 57 | 58 | return Redirect::to('/'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Http/Middleware/Admin.php: -------------------------------------------------------------------------------- 1 | user()->is_admin === 1) 19 | { 20 | return $next($request); 21 | } 22 | 23 | abort(403); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | |string> 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => [ 21 | 'required', 22 | 'string', 23 | 'lowercase', 24 | 'email', 25 | 'max:255', 26 | Rule::unique(User::class)->ignore($this->user()->id), 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Jobs/ItemNewStatusNotification.php: -------------------------------------------------------------------------------- 1 | item->user->email => 1, 34 | ); 35 | 36 | foreach($this->item->comments as $comment) { 37 | $receivers[$comment->user->email] = 1; 38 | } 39 | 40 | foreach($receivers as $email => $notify) { 41 | Mail::to($email)->send(new ItemNewStatus($this->item, $this->oldStatus, $this->newStatus)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Jobs/SendNewCommentNotification.php: -------------------------------------------------------------------------------- 1 | item->user->email => 1, 34 | ); 35 | 36 | foreach($this->item->comments as $comment) { 37 | $receivers[$comment->user->email] = 1; 38 | } 39 | 40 | unset($receivers[$this->comment->user->email]); 41 | 42 | // print_r($receivers); 43 | 44 | foreach($receivers as $email => $notify) { 45 | Mail::to($email)->send(new NewItemComment($this->item, $this->comment)); 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Livewire/ItemStatusSelector.php: -------------------------------------------------------------------------------- 1 | item = $item; 17 | $this->status = $this->item->status; 18 | } 19 | 20 | public function updatedStatus() 21 | { 22 | $comment = $this->item->comments()->create([ 23 | 'user_id' => auth()->id(), 24 | 'text' => $this->item->status . ' -> ' . $this->status, 25 | ]); 26 | 27 | ItemNewStatusNotification::dispatch( 28 | $this->item, 29 | $this->item->status, 30 | $this->status 31 | ); 32 | 33 | $this->item->update([ 34 | 'status' => $this->status 35 | ]); 36 | 37 | } 38 | 39 | public function render() 40 | { 41 | return <<<'HTML' 42 |
43 | 44 |
45 | HTML; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Mail/ItemNewStatus.php: -------------------------------------------------------------------------------- 1 | item->title, 36 | ); 37 | } 38 | 39 | /** 40 | * Get the message content definition. 41 | */ 42 | public function content(): Content 43 | { 44 | return new Content( 45 | view: 'emails.item-new-status', 46 | with: [ 47 | 'item_name' => $this->item->title, 48 | 'old_status' => $this->oldStatus, 49 | 'new_status' => $this->newStatus, 50 | 'item_url' => route('items.show', ['project' => $this->item->project, 'item' => $this->item->uuid]), 51 | ] 52 | ); 53 | } 54 | 55 | /** 56 | * Get the attachments for the message. 57 | * 58 | * @return array 59 | */ 60 | public function attachments(): array 61 | { 62 | return []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Mail/NewItemComment.php: -------------------------------------------------------------------------------- 1 | item->title, 36 | ); 37 | } 38 | 39 | /** 40 | * Get the message content definition. 41 | */ 42 | public function content(): Content 43 | { 44 | return new Content( 45 | view: 'emails.item-new-comment', 46 | with: [ 47 | 'item_name' => $this->item->title, 48 | 'comment_text' => $this->comment->text, 49 | 'author_name' => $this->comment->user->name, 50 | 'item_url' => route('items.show', ['project' => $this->item->project, 'item' => $this->item->uuid]), 51 | ] 52 | ); 53 | } 54 | 55 | /** 56 | * Get the attachments for the message. 57 | * 58 | * @return array 59 | */ 60 | public function attachments(): array 61 | { 62 | return []; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Models/Comment.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 18 | } 19 | 20 | public function item() 21 | { 22 | return $this->belongsTo(Item::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/Creative.php: -------------------------------------------------------------------------------- 1 | belongsTo(Item::class); 23 | } 24 | 25 | public function display(int $maxHeight = 300) 26 | { 27 | if($this->type === 'IMAGE') 28 | { 29 | return "path) . "' 30 | class='object-cover hover:object-contain hover:outline outline-gray-300 hover:cursor-pointer' 31 | style='max-height: {{$maxHeight}}px; aspect-ratio: 1/1;'>"; 32 | } 33 | if($this->type === 'VIDEO') 34 | { 35 | return ""; 41 | 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Models/Destination.php: -------------------------------------------------------------------------------- 1 | belongsTo(Item::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Models/Project.php: -------------------------------------------------------------------------------- 1 | logo ) 20 | { 21 | return asset('storage/' . $this->logo); 22 | } 23 | return asset('images/project-default.png'); 24 | } 25 | 26 | public function items() 27 | { 28 | return $this->hasMany(Item::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 13 | use HasFactory, Notifiable; 14 | 15 | /** 16 | * The attributes that are mass assignable. 17 | * 18 | * @var list 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | 'github_token', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var list 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * Get the attributes that should be cast. 39 | * 40 | * @return array 41 | */ 42 | protected function casts(): array 43 | { 44 | return [ 45 | 'email_verified_at' => 'datetime', 46 | 'password' => 'hashed', 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Policies/ItemPolicy.php: -------------------------------------------------------------------------------- 1 | check(); 14 | } 15 | 16 | /** 17 | * Determine whether the user can update the model. 18 | */ 19 | public function update(User $user, Item $item): bool 20 | { 21 | return $user->id === $item->user_id || $user->is_admin === 1; 22 | } 23 | 24 | /** 25 | * Determine whether the user can delete the model. 26 | */ 27 | public function delete(User $user, Item $item): bool 28 | { 29 | return $user->id === $item->user_id || $user->is_admin === 1; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | user()->is_admin === 1; 28 | }); 29 | Gate::define('edit-project', function(User $user, Project $project){ 30 | return auth()->user()->is_admin === 1 || $project->user_id == auth()->id(); 31 | }); 32 | Gate::define('edit-item', function(User $user, Item $item){ 33 | return auth()->user()->is_admin === 1 || $item->user_id == auth()->id(); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Services/ChatGPTService.php: -------------------------------------------------------------------------------- 1 | $title, 19 | 'story' => $story, 20 | ]; 21 | } 22 | 23 | static public function optimizeItem(Item $item, string $comment): void 24 | { 25 | $result = OpenAI::chat()->create([ 26 | 'model' => 'gpt-3.5-turbo', 27 | 'messages' => [ 28 | ['role' => 'user', 'content' => "Optimiere folgende vorhandene UserStory anhand eines Kommentars. Gib mir nur das Markdown zurück ohne Einleitungstext und Abschlusstext."], 29 | ['role' => 'user', 'content' => "Die UserStory: " . $item->story], 30 | ['role' => 'user', 'content' => "Das Kommentar: " . $comment], 31 | ], 32 | ]); 33 | 34 | $item->story = $result->choices[0]->message->content; 35 | $item->translated = self::translateAndMarkdown($item->title, $item->story); 36 | $item->save(); 37 | } 38 | 39 | static private function prompt(string $prompt) :string 40 | { 41 | $result = OpenAI::chat()->create([ 42 | 'model' => 'gpt-3.5-turbo', 43 | 'messages' => [ 44 | ['role' => 'user', 'content' => $prompt], 45 | ], 46 | ]); 47 | \Log::info(print_r($result, 1)); 48 | return $result->choices[0]->message->content; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Services/ItemUpvoteService.php: -------------------------------------------------------------------------------- 1 | id . '-item-' . $item->id) ) 14 | { 15 | return false; 16 | } 17 | 18 | return true; 19 | } 20 | 21 | public static function upvote(User $user, Item $item) 22 | { 23 | $item->increment('voting'); 24 | Cache::put('user-' . $user->id . '-item-' . $item->id, 1); 25 | } 26 | 27 | public static function downvote(User $user, Item $item) 28 | { 29 | $item->decrement('voting'); 30 | Cache::pull('user-' . $user->id . '-item-' . $item->id, 1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Services/QueueWorker.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 10 | web: __DIR__.'/../routes/web.php', 11 | commands: __DIR__.'/../routes/console.php', 12 | health: '/up', 13 | ) 14 | ->withMiddleware(function (Middleware $middleware) { 15 | $middleware->alias([ 16 | 'admin' => Admin::class, 17 | ]); 18 | }) 19 | ->withExceptions(function (Exceptions $exceptions) { 20 | // 21 | })->create(); 22 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | env('OPENAI_API_KEY'), 16 | 'organization' => env('OPENAI_ORGANIZATION'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Request Timeout 21 | |-------------------------------------------------------------------------- 22 | | 23 | | The timeout may be used to specify the maximum number of seconds to wait 24 | | for a response. By default, the client will time out after 30 seconds. 25 | */ 26 | 27 | 'request_timeout' => env('OPENAI_REQUEST_TIMEOUT', 30), 28 | ]; 29 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'allowed_domain' => env('ALLOWED_REGISTRATION_DOMAIN') 18 | ], 19 | 20 | 'postmark' => [ 21 | 'token' => env('POSTMARK_TOKEN'), 22 | ], 23 | 24 | 'ses' => [ 25 | 'key' => env('AWS_ACCESS_KEY_ID'), 26 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 27 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 28 | ], 29 | 30 | 'resend' => [ 31 | 'key' => env('RESEND_KEY'), 32 | ], 33 | 34 | 'slack' => [ 35 | 'notifications' => [ 36 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 37 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 38 | ], 39 | ], 40 | 41 | 'github' => [ 42 | 'client_id' => env('GITHUB_CLIENT_ID'), 43 | 'client_secret' => env('GITHUB_CLIENT_SECRET'), 44 | 'redirect' => env('GITHUB_REDIRECT_URI'), 45 | ], 46 | 47 | ]; 48 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->boolean('is_admin')->default(0); 21 | $table->rememberToken(); 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('password_reset_tokens', function (Blueprint $table) { 26 | $table->string('email')->primary(); 27 | $table->string('token'); 28 | $table->timestamp('created_at')->nullable(); 29 | }); 30 | 31 | Schema::create('sessions', function (Blueprint $table) { 32 | $table->string('id')->primary(); 33 | $table->foreignId('user_id')->nullable()->index(); 34 | $table->string('ip_address', 45)->nullable(); 35 | $table->text('user_agent')->nullable(); 36 | $table->longText('payload'); 37 | $table->integer('last_activity')->index(); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | */ 44 | public function down(): void 45 | { 46 | Schema::dropIfExists('users'); 47 | Schema::dropIfExists('password_reset_tokens'); 48 | Schema::dropIfExists('sessions'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2025_01_28_155420_create_projects_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->foreignId('user_id'); 19 | 20 | $table->string('logo')->nullable(); 21 | $table->string('name'); 22 | $table->text('description'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('projects'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2025_01_28_155641_create_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->foreignId('project_id')->constrained()->onDelete('cascade'); 19 | $table->foreignId('user_id'); 20 | 21 | $table->string('title'); 22 | $table->text('story'); 23 | $table->string('status')->default('CREATED'); 24 | $table->string('priority')->default('LOW'); 25 | $table->string('type')->default('TASK'); 26 | $table->integer('voting')->default(0); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('items'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2025_01_28_193840_add_template_to_projects_table.php: -------------------------------------------------------------------------------- 1 | text('template')->default('

Was wünscht du dir?

'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('projects', function (Blueprint $table) { 25 | // 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_01_28_215636_create_comments_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->foreignId('user_id'); 19 | $table->foreignId('item_id')->constrained()->onDelete('cascade'); 20 | $table->text('text'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('comments'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2025_02_06_223953_add_translated_column_to_items_table.php: -------------------------------------------------------------------------------- 1 | text('translated')->nullable()->after('story'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('items', function (Blueprint $table) { 25 | $table->dropColumn('translated'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_02_12_092747_change_column_in_items_table.php: -------------------------------------------------------------------------------- 1 | json('translated')->nullable()->after('story'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('items', function (Blueprint $table) { 25 | $table->text('translated')->nullable()->after('story'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_02_14_200600_add_uuid_to_items_table.php: -------------------------------------------------------------------------------- 1 | uuid('uuid')->after('id')->nullable(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('items', function (Blueprint $table) { 25 | // 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /database/migrations/2025_02_22_111529_create_destinations_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | 18 | $table->foreignId('item_id')->constrained('items')->onDelete('cascade'); 19 | $table->string('type'); // GITHUB, TASKIFY, Whatever 20 | $table->string('remote_id'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('destinations'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2025_03_08_212040_create_creatives_table.php: -------------------------------------------------------------------------------- 1 | uuid('id'); 16 | $table->timestamps(); 17 | $table->foreignUuid('item_id'); 18 | $table->string('name'); 19 | $table->string('path'); 20 | $table->string('type'); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | */ 27 | public function down(): void 28 | { 29 | Schema::dropIfExists('creatives'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/2025_03_20_170310_add_github_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('github_token')->nullable(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('users', function (Blueprint $table) { 25 | $table->dropColumn('github_token'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /lang/de/items.php: -------------------------------------------------------------------------------- 1 | 'Story anlegen', 5 | 'edit' => 'Story bearbeiten', 6 | 7 | 'in_progress' => 'In Arbeit', 8 | 'in_progress_abbr' => 'A', 9 | 'done' => 'Fertig', 10 | 'done_abbr' => 'F', 11 | 'open' => 'Offen', 12 | 'open_abbr' => 'O', 13 | 14 | 'markdown' => 'Markdown', 15 | 'comment' => 'Kommentieren', 16 | 'send_comment' => 'Kommentar absenden', 17 | 18 | 'low' => 'Niedrig', 19 | 'low_description' => 'Schönheits/Stylingfehler welche die Funktion des Systems nicht beeinflussen.', 20 | 'medium' => 'Mittel', 21 | 'medium_description' => 'Funktionen die für zukünftige Kunden oder Projektbenutzer wichtig werden können.', 22 | 'high' => 'Hoch', 23 | 'high_description' => 'Sollte als erstes von den normalen ToDos bearbeitet werden da die Funktion schnellstmöglich gebraucht wird.', 24 | 'critical' => 'Kritisch', 25 | 'critical_description' => 'Das System funktioniert nicht oder verhält sich in wichtigen Situationen komplett falsch.', 26 | 27 | 'bug' => 'Bug', 28 | 'bug_description' => 'Eine fehlerhafte Funktion im Projekt', 29 | 'feature' => 'Feature', 30 | 'feature_description' => 'Eine neue Funktion für das Projekt', 31 | 'task' => 'Aufgabe', 32 | 'task_description' => 'Generelle Aufgabe für das System die weder einen Fehler löst noch ein neues Feature hinzufügt.', 33 | ]; 34 | -------------------------------------------------------------------------------- /lang/de/pagination.php: -------------------------------------------------------------------------------- 1 | '« Zurück', 17 | 'next' => 'Weiter »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/de/passwords.php: -------------------------------------------------------------------------------- 1 | 'Dein Passwort wurde zurückgesetzt.', 18 | 'sent' => 'Wir haben dir den Link zum Zurücksetzen deines Passworts per E-Mail gesendet.', 19 | 'throttled' => 'Bitte warte, bevor du es erneut versuchst.', 20 | 'token' => 'Dieses Token zum Zurücksetzen des Passworts ist ungültig.', 21 | 'user' => "Wir können keinen Benutzer mit dieser E-Mail-Adresse finden.", 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /lang/de/profile.php: -------------------------------------------------------------------------------- 1 | 'Profil', 5 | ]; 6 | -------------------------------------------------------------------------------- /lang/de/projects.php: -------------------------------------------------------------------------------- 1 | 'Projektübersicht', 5 | 'show' => 'Projekt ansehen', 6 | 'create' => 'Projekt anlegen', 7 | 8 | 'logo' => 'Logo', 9 | 'description' => 'Beschreibung', 10 | 'story_format' => 'Story Format', 11 | 'save' => 'Speichern', 12 | 13 | 'back_to_overview' => 'Zurück zur Projektübersicht', 14 | 'create_story' => '✨ User Story erstellen ✨', 15 | 16 | 'back_to_project' => 'Zurück zu :project', 17 | ]; 18 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | "The credentials are incorrect.", 18 | "throttle" => 19 | "Too many login attempts. Please try again in :seconds seconds.", 20 | 21 | "name" => "Name", 22 | "email" => "Email", 23 | "company" => "Company", 24 | "password" => "Password", 25 | "confirm_password" => "Confirm Password", 26 | "already_registered" => "Already registered?", 27 | "register" => "Register", 28 | 29 | "remember_me" => "Remember me", 30 | "forgot_password" => "Forgot password?", 31 | "forgot_password_intro" => "Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.", 32 | "login" => "Login", 33 | "logout" => "Logout", 34 | "profile" => "Profile", 35 | "profile_information" => "Your account settings", 36 | 37 | "reset_password" => "Reset Password", 38 | "send_reset_password" => "Send Password Reset Email", 39 | 40 | "update_password" => "Update password", 41 | "password_info" => "Use a strong password with special characters to protect your account.", 42 | "current_password" => "Current password", 43 | "new_password" => "New password", 44 | 45 | "email_unverified" => "Your email address is not verified.", 46 | "resend_verification" => "Click here to receive a new verification email.", 47 | "verification_sent" => "A new verification link has been sent to your email.", 48 | 49 | "delete_account" => "Delete account", 50 | "delete_account_info" => "When your account is deleted, we will also delete all associated data. This cannot be recovered!", 51 | "delete_confirm" => "Are you sure you want to delete your account?", 52 | "cancel" => "Cancel", 53 | 54 | "admin" => "Admin", 55 | ]; 56 | -------------------------------------------------------------------------------- /lang/en/items.php: -------------------------------------------------------------------------------- 1 | 'Create Story', 5 | 'edit' => 'Edit Story', 6 | 7 | 'in_progress' => 'In Progress', 8 | 'in_progress_abbr' => 'P', 9 | 'done' => 'Done', 10 | 'done_abbr' => 'D', 11 | 'open' => 'Open', 12 | 'open_abbr' => 'O', 13 | 14 | 'markdown' => 'Markdown', 15 | 'comment' => 'Comment', 16 | 'send_comment' => 'Send comment', 17 | 18 | 'low' => 'Low', 19 | 'low_description' => 'Cosmetic/styling issues that do not affect the functionality of the system.', 20 | 'medium' => 'Medium', 21 | 'medium_description' => 'Features that may become important for future customers or project users.', 22 | 'high' => 'High', 23 | 'high_description' => 'Should be prioritized over regular tasks as the functionality is needed as soon as possible.', 24 | 'critical' => 'Critical', 25 | 'critical_description' => 'The system is not functioning or behaves completely incorrectly in critical situations.', 26 | 27 | 'bug' => 'Bug', 28 | 'bug_description' => 'A malfunctioning feature in the project.', 29 | 'feature' => 'Feature', 30 | 'feature_description' => 'A new functionality for the project.', 31 | 'task' => 'Task', 32 | 'task_description' => 'A general task for the system that neither fixes a bug nor adds a new feature.', 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset.', 17 | 'sent' => 'We have emailed your password reset link.', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /lang/en/profile.php: -------------------------------------------------------------------------------- 1 | 'Profile', 5 | ]; 6 | -------------------------------------------------------------------------------- /lang/en/projects.php: -------------------------------------------------------------------------------- 1 | 'Project Overview', 5 | 'show' => 'View Project', 6 | 'create' => 'Create Project', 7 | 8 | 'logo' => 'Logo', 9 | 'description' => 'Description', 10 | 'story_format' => 'Story Format', 11 | 'save' => 'Save', 12 | 13 | 'back_to_overview' => 'Back to Overview', 14 | 'create_story' => '✨ Create User Story ✨', 15 | 16 | 'back_to_project' => 'Back to :project', 17 | ]; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.2", 10 | "alpinejs": "^3.4.2", 11 | "autoprefixer": "^10.4.2", 12 | "axios": "^1.7.4", 13 | "concurrently": "^9.0.1", 14 | "laravel-vite-plugin": "^1.0", 15 | "postcss": "^8.4.31", 16 | "tailwindcss": "^3.1.0", 17 | "vite": "^6.0" 18 | }, 19 | "dependencies": { 20 | "tinymce": "^7.6.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests/Unit 10 | 11 | 12 | tests/Feature 13 | 14 | 15 | 16 | 17 | app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1aWebmarketing/infiniteloop/a9e316b86b51fbe111f53a84f783792614918fe9/public/favicon.png -------------------------------------------------------------------------------- /public/images/project-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1aWebmarketing/infiniteloop/a9e316b86b51fbe111f53a84f783792614918fe9/public/images/project-default.png -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/js/tinymce/.npmignore: -------------------------------------------------------------------------------- 1 | composer.json 2 | bower.json 3 | -------------------------------------------------------------------------------- /public/js/tinymce/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinymce", 3 | "description": "Web based JavaScript HTML WYSIWYG editor control.", 4 | "license": "GPL-2.0-or-later", 5 | "keywords": [ 6 | "wysiwyg", 7 | "tinymce", 8 | "richtext", 9 | "javascript", 10 | "html", 11 | "text", 12 | "rich editor", 13 | "rich text editor", 14 | "rte", 15 | "rich text", 16 | "contenteditable", 17 | "editing" 18 | ], 19 | "homepage": "https://www.tiny.cloud/", 20 | "ignore": [ 21 | "README.md", 22 | "composer.json", 23 | "package.json", 24 | ".npmignore", 25 | "CHANGELOG.md" 26 | ] 27 | } -------------------------------------------------------------------------------- /public/js/tinymce/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinymce/tinymce", 3 | "version": "7.5.0", 4 | "description": "Web based JavaScript HTML WYSIWYG editor control.", 5 | "license": [ 6 | "GPL-2.0-or-later" 7 | ], 8 | "keywords": [ 9 | "wysiwyg", 10 | "tinymce", 11 | "richtext", 12 | "javascript", 13 | "html", 14 | "text", 15 | "rich editor", 16 | "rich text editor", 17 | "rte", 18 | "rich text", 19 | "contenteditable", 20 | "editing" 21 | ], 22 | "homepage": "https://www.tiny.cloud/", 23 | "type": "component", 24 | "extra": { 25 | "component": { 26 | "scripts": [ 27 | "tinymce.js", 28 | "plugins/*/plugin.js", 29 | "themes/*/theme.js", 30 | "models/*/model.js", 31 | "icons/*/icons.js" 32 | ], 33 | "files": [ 34 | "tinymce.min.js", 35 | "plugins/*/plugin.min.js", 36 | "themes/*/theme.min.js", 37 | "models/*/model.min.js", 38 | "skins/**", 39 | "icons/*/icons.min.js" 40 | ] 41 | } 42 | }, 43 | "archive": { 44 | "exclude": [ 45 | "README.md", 46 | "bower.js", 47 | "package.json", 48 | ".npmignore", 49 | "CHANGELOG.md" 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /public/js/tinymce/icons/default/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "default" icons for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/icons/default') 5 | // ES2015: 6 | // import 'tinymce/icons/default' 7 | require('./icons.js'); -------------------------------------------------------------------------------- /public/js/tinymce/license.md: -------------------------------------------------------------------------------- 1 | # Software License Agreement 2 | 3 | **TinyMCE** – [](https://github.com/tinymce/tinymce) 4 | Copyright (c) 2024, Ephox Corporation DBA Tiny Technologies, Inc. 5 | 6 | Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html). 7 | -------------------------------------------------------------------------------- /public/js/tinymce/models/dom/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "dom" model for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/models/dom') 5 | // ES2015: 6 | // import 'tinymce/models/dom' 7 | require('./model.js'); -------------------------------------------------------------------------------- /public/js/tinymce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tinymce", 3 | "version": "7.5.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/tinymce/tinymce.git", 7 | "directory": "modules/tinymce" 8 | }, 9 | "description": "Web based JavaScript HTML WYSIWYG editor control.", 10 | "author": "Ephox Corporation DBA Tiny Technologies, Inc", 11 | "main": "tinymce.js", 12 | "types": "tinymce.d.ts", 13 | "license": "GPL-2.0-or-later", 14 | "keywords": [ 15 | "wysiwyg", 16 | "tinymce", 17 | "richtext", 18 | "javascript", 19 | "html", 20 | "text", 21 | "rich editor", 22 | "rich text editor", 23 | "rte", 24 | "rich text", 25 | "contenteditable", 26 | "editing" 27 | ], 28 | "homepage": "https://www.tiny.cloud/", 29 | "bugs": { 30 | "url": "https://github.com/tinymce/tinymce/issues" 31 | } 32 | } -------------------------------------------------------------------------------- /public/js/tinymce/plugins/accordion/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "accordion" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/accordion') 5 | // ES2015: 6 | // import 'tinymce/plugins/accordion' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/advlist/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "advlist" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/advlist') 5 | // ES2015: 6 | // import 'tinymce/plugins/advlist' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/anchor/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "anchor" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/anchor') 5 | // ES2015: 6 | // import 'tinymce/plugins/anchor' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/autolink/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "autolink" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/autolink') 5 | // ES2015: 6 | // import 'tinymce/plugins/autolink' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/autoresize/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "autoresize" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/autoresize') 5 | // ES2015: 6 | // import 'tinymce/plugins/autoresize' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/autosave/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "autosave" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/autosave') 5 | // ES2015: 6 | // import 'tinymce/plugins/autosave' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/charmap/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "charmap" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/charmap') 5 | // ES2015: 6 | // import 'tinymce/plugins/charmap' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/code/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "code" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/code') 5 | // ES2015: 6 | // import 'tinymce/plugins/code' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/code/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 7.5.0 (2024-11-06) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | const setContent = (editor, html) => { 11 | editor.focus(); 12 | editor.undoManager.transact(() => { 13 | editor.setContent(html); 14 | }); 15 | editor.selection.setCursorLocation(); 16 | editor.nodeChanged(); 17 | }; 18 | const getContent = editor => { 19 | return editor.getContent({ source_view: true }); 20 | }; 21 | 22 | const open = editor => { 23 | const editorContent = getContent(editor); 24 | editor.windowManager.open({ 25 | title: 'Source Code', 26 | size: 'large', 27 | body: { 28 | type: 'panel', 29 | items: [{ 30 | type: 'textarea', 31 | name: 'code' 32 | }] 33 | }, 34 | buttons: [ 35 | { 36 | type: 'cancel', 37 | name: 'cancel', 38 | text: 'Cancel' 39 | }, 40 | { 41 | type: 'submit', 42 | name: 'save', 43 | text: 'Save', 44 | primary: true 45 | } 46 | ], 47 | initialData: { code: editorContent }, 48 | onSubmit: api => { 49 | setContent(editor, api.getData().code); 50 | api.close(); 51 | } 52 | }); 53 | }; 54 | 55 | const register$1 = editor => { 56 | editor.addCommand('mceCodeEditor', () => { 57 | open(editor); 58 | }); 59 | }; 60 | 61 | const register = editor => { 62 | const onAction = () => editor.execCommand('mceCodeEditor'); 63 | editor.ui.registry.addButton('code', { 64 | icon: 'sourcecode', 65 | tooltip: 'Source code', 66 | onAction 67 | }); 68 | editor.ui.registry.addMenuItem('code', { 69 | icon: 'sourcecode', 70 | text: 'Source code', 71 | onAction 72 | }); 73 | }; 74 | 75 | var Plugin = () => { 76 | global.add('code', editor => { 77 | register$1(editor); 78 | register(editor); 79 | return {}; 80 | }); 81 | }; 82 | 83 | Plugin(); 84 | 85 | })(); 86 | -------------------------------------------------------------------------------- /public/js/tinymce/plugins/code/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 7.5.0 (2024-11-06) 3 | */ 4 | !function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}(); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/codesample/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "codesample" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/codesample') 5 | // ES2015: 6 | // import 'tinymce/plugins/codesample' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/directionality/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "directionality" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/directionality') 5 | // ES2015: 6 | // import 'tinymce/plugins/directionality' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/emoticons/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "emoticons" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/emoticons') 5 | // ES2015: 6 | // import 'tinymce/plugins/emoticons' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/fullscreen/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "fullscreen" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/fullscreen') 5 | // ES2015: 6 | // import 'tinymce/plugins/fullscreen' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/help/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "help" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/help') 5 | // ES2015: 6 | // import 'tinymce/plugins/help' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/image/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "image" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/image') 5 | // ES2015: 6 | // import 'tinymce/plugins/image' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/importcss/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "importcss" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/importcss') 5 | // ES2015: 6 | // import 'tinymce/plugins/importcss' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/insertdatetime/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "insertdatetime" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/insertdatetime') 5 | // ES2015: 6 | // import 'tinymce/plugins/insertdatetime' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/link/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "link" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/link') 5 | // ES2015: 6 | // import 'tinymce/plugins/link' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/lists/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "lists" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/lists') 5 | // ES2015: 6 | // import 'tinymce/plugins/lists' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/media/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "media" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/media') 5 | // ES2015: 6 | // import 'tinymce/plugins/media' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/nonbreaking/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "nonbreaking" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/nonbreaking') 5 | // ES2015: 6 | // import 'tinymce/plugins/nonbreaking' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/nonbreaking/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 7.5.0 (2024-11-06) 3 | */ 4 | !function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=n=>e=>typeof e===n,o=e("boolean"),a=e("number"),t=n=>e=>e.options.get(n),i=t("nonbreaking_force_tab"),s=t("nonbreaking_wrap"),r=(n,e)=>{let o="";for(let a=0;a{const o=s(n)||n.plugins.visualchars?`${r(" ",e)}`:r(" ",e);n.undoManager.transact((()=>n.insertContent(o)))};var l=tinymce.util.Tools.resolve("tinymce.util.VK");const u=n=>e=>{const o=()=>{e.setEnabled(n.selection.isEditable())};return n.on("NodeChange",o),o(),()=>{n.off("NodeChange",o)}};n.add("nonbreaking",(n=>{(n=>{const e=n.options.register;e("nonbreaking_force_tab",{processor:n=>o(n)?{value:n?3:0,valid:!0}:a(n)?{value:n,valid:!0}:{valid:!1,message:"Must be a boolean or number."},default:!1}),e("nonbreaking_wrap",{processor:"boolean",default:!0})})(n),(n=>{n.addCommand("mceNonBreaking",(()=>{c(n,1)}))})(n),(n=>{const e=()=>n.execCommand("mceNonBreaking");n.ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:e,onSetup:u(n)}),n.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:e,onSetup:u(n)})})(n),(n=>{const e=i(n);e>0&&n.on("keydown",(o=>{if(o.keyCode===l.TAB&&!o.isDefaultPrevented()){if(o.shiftKey)return;o.preventDefault(),o.stopImmediatePropagation(),c(n,e)}}))})(n)}))}(); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/pagebreak/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "pagebreak" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/pagebreak') 5 | // ES2015: 6 | // import 'tinymce/plugins/pagebreak' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/pagebreak/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 7.5.0 (2024-11-06) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env");const t=e=>a=>a.options.get(e),n=t("pagebreak_separator"),o=t("pagebreak_split_block"),r="mce-pagebreak",s=e=>{const t=``;return e?`

${t}

`:t},c=e=>a=>{const t=()=>{a.setEnabled(e.selection.isEditable())};return e.on("NodeChange",t),t(),()=>{e.off("NodeChange",t)}};e.add("pagebreak",(e=>{(e=>{const a=e.options.register;a("pagebreak_separator",{processor:"string",default:"\x3c!-- pagebreak --\x3e"}),a("pagebreak_split_block",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mcePageBreak",(()=>{e.insertContent(s(o(e)))}))})(e),(e=>{const a=()=>e.execCommand("mcePageBreak");e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:a,onSetup:c(e)}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:a,onSetup:c(e)})})(e),(e=>{const a=n(e),t=()=>o(e),c=new RegExp(a.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,(e=>"\\"+e)),"gi");e.on("BeforeSetContent",(e=>{e.content=e.content.replace(c,s(t()))})),e.on("PreInit",(()=>{e.serializer.addNodeFilter("img",(n=>{let o,s,c=n.length;for(;c--;)if(o=n[c],s=o.attr("class"),s&&-1!==s.indexOf(r)){const n=o.parent;if(n&&e.schema.getBlockElements()[n.name]&&t()){n.type=3,n.value=a,n.raw=!0,o.remove();continue}o.type=3,o.value=a,o.raw=!0}}))}))})(e),(e=>{e.on("ResolveName",(a=>{"IMG"===a.target.nodeName&&e.dom.hasClass(a.target,r)&&(a.name="pagebreak")}))})(e)}))}(); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/preview/index.js: -------------------------------------------------------------------------------- 1 | // Exports the "preview" plugin for usage with module loaders 2 | // Usage: 3 | // CommonJS: 4 | // require('tinymce/plugins/preview') 5 | // ES2015: 6 | // import 'tinymce/plugins/preview' 7 | require('./plugin.js'); -------------------------------------------------------------------------------- /public/js/tinymce/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 7.5.0 (2024-11-06) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env"),n=tinymce.util.Tools.resolve("tinymce.util.Tools");const o=e=>t=>t.options.get(e),i=o("content_style"),s=o("content_css_cors"),c=o("body_class"),r=o("body_id");e.add("preview",(e=>{(e=>{e.addCommand("mcePreview",(()=>{(e=>{const o=(e=>{var o;let a="";const l=e.dom.encode,d=null!==(o=i(e))&&void 0!==o?o:"";a+='';const m=s(e)?' crossorigin="anonymous"':"";n.each(e.contentCSS,(t=>{a+='"})),d&&(a+='");const y=r(e),u=c(e),v=' 2 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/comments.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/components/icons/delete.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/components/icons/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/items.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/markdown.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/right-arrow.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/up-arrow-green.blade.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/views/components/icons/up-arrow.blade.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/views/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props(['messages']) 2 | 3 | @if ($messages) 4 |
    merge(['class' => 'text-sm text-red-600 space-y-1']) }}> 5 | @foreach ((array) $messages as $message) 6 |
  • {{ $message }}
  • 7 | @endforeach 8 |
9 | @endif 10 | -------------------------------------------------------------------------------- /resources/views/components/input-group.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'id' => '', 3 | 'label' => false, 4 | 'disabled' => false, 5 | 'messages' => NULL, 6 | 'required' => NULL, 7 | 'readonly' => NULL, 8 | 'type' => 'text', 9 | 'name' => '', 10 | 'description' => '', 11 | 'wrapperClass' => '', 12 | ]) 13 | @php 14 | $baseClasses = 'border-gray-300 w-full pb-2 pt-3 focus:border-main focus:ring-main-500 rounded-sm shadow-sm'; 15 | if( $readonly || $disabled ) 16 | { 17 | $baseClasses = 'border-gray-300 bg-gray-200 w-full pb-2 pt-3 focus:border-transparent focus:ring-0 rounded-sm shadow-sm'; 18 | } 19 | @endphp 20 |
21 |
22 | @if($label) 23 | 29 | @endif 30 | merge(['class' => $baseClasses]) !!}> 32 | 33 | @if($description !== '') 34 |

{{ $description }}

35 | @endif 36 | 37 | @if($errors->has($name)) 38 |
    39 | @foreach ($errors->get($name) as $error) 40 |
  • {{ $error }}
  • 41 | @endforeach 42 |
43 | @endif 44 |
45 |
46 | -------------------------------------------------------------------------------- /resources/views/components/input-label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/views/components/item-priority.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name' => '', 3 | 'value' => '', 4 | ]) 5 |
12 | 13 | 21 | 22 | 30 | 31 | 39 | 40 | 48 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /resources/views/components/item-table.blade.php: -------------------------------------------------------------------------------- 1 |
merge(['class' => 'flex gap-4 mb-2 border-b pb-2 px-2 pt-2']) }}> 2 |
3 |
4 | @csrf 5 | 12 |
13 |

{{ $item->voting }}

14 |
15 | 16 |
17 |

{{ $item->title }} {{ formatDateTime($item->created_at) }}

18 |
    19 |
  • {!! $item->typePillHtml() !!}
  • 20 |
  • {!! $item->priorityPillHtml() !!}
  • 21 |
22 |
23 | 24 |
25 |

{{ $item->comments()->count() }}

26 |
27 |
28 | -------------------------------------------------------------------------------- /resources/views/components/item-type.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name' => '', 3 | 'value' => '', 4 | ]) 5 |
12 | 13 | 21 | 22 | 30 | 31 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/primary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/primary-link-button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'href' => '', 3 | ]) 4 | merge(['type' => 'submit', 'type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}> 5 | {{ $slot }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out' 6 | : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/select-group.blade.php: -------------------------------------------------------------------------------- 1 | @props(['id' => '' , 'label' => false , 'disabled' => false, 'messages' => NULL, 'required' => NULL , 'name' => '', 'options' => '', 'value' => '' ]) 2 | 3 | @php 4 | $renderOptions = array(); 5 | if(!empty($options)) 6 | { 7 | $tmpOptions = explode(";", $options); 8 | 9 | foreach($tmpOptions as $option) 10 | { 11 | list($tmpVal, $tmpLabel) = explode(":", $option); 12 | $renderOptions[$tmpVal] = $tmpLabel; 13 | } 14 | } 15 | @endphp 16 | 17 |
18 |
19 | @if($label) 20 | 26 | @endif 27 | 37 | 38 | @if($errors->has($name)) 39 |
    40 | @foreach ($errors->get($name) as $error) 41 |
  • {{ $error }}
  • 42 | @endforeach 43 |
44 | @endif 45 |
46 |
47 | -------------------------------------------------------------------------------- /resources/views/components/text-input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'border-gray-300 focus:border-primary focus:ring-primary rounded-md shadow-sm mb-2']) !!}> 4 | -------------------------------------------------------------------------------- /resources/views/components/textarea-group.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'id' => '' , 3 | 'label' => false , 4 | 'disabled' => false, 5 | 'messages' => NULL, 6 | 'required' => NULL , 7 | 'name' => '', 8 | 'description' => '' 9 | ]) 10 | 11 |
12 |
13 | @if($label) 14 | 20 | @endif 21 | 26 | 27 | @if($description !== '') 28 |

{{ $description }}

29 | @endif 30 | 31 | @if($errors->has($name)) 32 |
    33 | @foreach ($errors->get($name) as $error) 34 |
  • {{ $error }}
  • 35 | @endforeach 36 |
37 | @endif 38 |
39 |
40 | -------------------------------------------------------------------------------- /resources/views/components/textarea.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name' => '', 3 | ]) 4 | 5 | -------------------------------------------------------------------------------- /resources/views/emails/item-new-comment.blade.php: -------------------------------------------------------------------------------- 1 |

{{ $comment_text }}

2 |

Von: {{ $author_name }}

3 |

Link zur User Story

4 |
5 | infiniteloop 6 | -------------------------------------------------------------------------------- /resources/views/emails/item-new-status.blade.php: -------------------------------------------------------------------------------- 1 |

{{ $old_status }} > {{ $new_status }}

2 |

Link zur User Story

3 |
4 | infiniteloop 5 | -------------------------------------------------------------------------------- /resources/views/errors/401.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Unauthorized')) 4 | @section('code', '401') 5 | @section('message', __('Unauthorized')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/402.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Payment Required')) 4 | @section('code', '402') 5 | @section('message', __('Payment Required')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/403.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Forbidden')) 4 | @section('code', '403') 5 | @section('message', __($exception->getMessage() ?: 'Forbidden')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/404.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Not Found')) 4 | @section('code', '404') 5 | @section('message', __('Not Found')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/419.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Page Expired')) 4 | @section('code', '419') 5 | @section('message', __('Page Expired')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/429.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Too Many Requests')) 4 | @section('code', '429') 5 | @section('message', __('Too Many Requests')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Server Error')) 4 | @section('code', '500') 5 | @section('message', __('Server Error')) 6 | -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | We'll Be Back Soon! 7 | 30 | 31 | 32 |
33 |

🚧 We'll Be Back Soon!

34 |

Our website is currently undergoing maintenance. We should be back shortly. Thank you for your patience!

35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /resources/views/errors/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @yield('title') 8 | 9 | 10 | 43 | 44 | 45 |
46 |
47 |
48 | @yield('message') 49 |
50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /resources/views/items/form.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
7 |
8 | Zurück zu {{ $item->project->name }} 9 | {{ $item->project->name }} - Feature vorschlagen 10 |
11 |
12 | 13 |
14 | @csrf 15 | @if($item->exists) 16 | @method('PUT') 17 | @endif 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ old('story', $item->story) }} 27 | 28 | 29 | 30 | Typ 31 | 32 | 33 | 34 | Priorität 35 | 36 | 37 | 38 | Speichern 39 | 40 |
41 |
42 | 43 | 44 | Medien 45 |
46 | @csrf 47 |
48 |
49 | 50 |
51 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'adfinity') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @vite(['resources/css/app.css', 'resources/js/app.js']) 18 | @livewireStyles 19 | 20 | 21 |
22 | 23 | 24 |
25 | {{ $slot }} 26 |
27 |
28 | @livewireScripts 29 | 30 | 31 | -------------------------------------------------------------------------------- /resources/views/pages/changelog.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Changelog 5 |
6 |
7 | 8 | 9 |
10 | {!! $changelog !!} 11 |
12 | 13 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /resources/views/profile/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Profil 5 |
6 |
7 | 8 |
9 |
10 | 11 | @include('profile.partials.update-profile-information-form') 12 |
13 | 14 | 15 | 16 | @include('profile.partials.update-password-form') 17 | 18 | 19 | 20 | @include('profile.partials.delete-user-form') 21 | 22 |
23 | 24 |
25 | -------------------------------------------------------------------------------- /resources/views/profile/partials/delete-user-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Delete Account') }} 5 |

6 | 7 |

8 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 9 |

10 |
11 | 12 | {{ __('Delete Account') }} 16 | 17 | 18 |
19 | @csrf 20 | @method('delete') 21 | 22 |

23 | {{ __('Are you sure you want to delete your account?') }} 24 |

25 | 26 |

27 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 28 |

29 | 30 |
31 | 32 | 33 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | {{ __('Cancel') }} 47 | 48 | 49 | 50 | {{ __('Delete Account') }} 51 | 52 |
53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /resources/views/profile/partials/update-password-form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ __('Update Password') }} 5 |

6 | 7 |

8 | {{ __('Ensure your account is using a long, random password to stay secure.') }} 9 |

10 |
11 | 12 |
13 | @csrf 14 | @method('put') 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 |
33 | 34 |
35 | {{ __('Save') }} 36 | 37 | @if (session('status') === 'password-updated') 38 |

{{ __('Saved.') }}

45 | @endif 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /resources/views/projects/form.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | {{ __('projects.create') }} 6 |
7 |
8 | 9 | 10 |
11 | @csrf 12 | @if($project->exists) 13 | @method('PUT') 14 | @endif 15 | 16 | 17 | 18 | 19 | {{ __('projects.logo') }} 20 | @if( $project->logo ) 21 |
22 | 23 |
24 | @endif 25 | 26 | 27 | 28 | {{ __('projects.description') }} 29 | 30 | {{ old('description', $project->description) }} 31 | 32 | {{ __('projects.story_format') }} 33 | 34 | {{ old('template', $project->template) }} 35 | 36 | {{ __('projects.save') }} 37 | 38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /resources/views/projects/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
7 |
8 | {{ __('projects.back_to_overview') }} 9 | {{ $project->name }} 10 |
11 |
12 | 13 |
14 |
15 | {{--Alle anzeigen 16 | Abgeschlossene Items --}} 17 |
18 | {{ __('projects.create_story') }} 19 |
20 | 21 | 22 | @if( $activeItems->count() ) 23 | {{ __('items.in_progress') }} 24 | @foreach($activeItems as $activeItem) 25 | 26 | @endforeach 27 | @endif 28 | 29 | @if( $createdItems->count() ) 30 | {{ __('items.open') }} 31 | @foreach($createdItems as $createdItem) 32 | 33 | @endforeach 34 | @endif 35 | 36 | @if( $doneItems->count() ) 37 | {{ __('items.done') }} 38 | @foreach($doneItems as $doneItem) 39 | 40 | @endforeach 41 | @endif 42 |
43 |
44 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | everyMinute()->name('QueueWorker::_invoke'); 7 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | './app/Models/*.php', 11 | ], 12 | 13 | theme: { 14 | extend: { 15 | fontFamily: { 16 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 17 | }, 18 | }, 19 | }, 20 | 21 | plugins: [forms], 22 | }; 23 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users can not authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | 34 | test('users can logout', function () { 35 | $user = User::factory()->create(); 36 | 37 | $response = $this->actingAs($user)->post('/logout'); 38 | 39 | $this->assertGuest(); 40 | $response->assertRedirect('/'); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | unverified()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/verify-email'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('email can be verified', function () { 17 | $user = User::factory()->unverified()->create(); 18 | 19 | Event::fake(); 20 | 21 | $verificationUrl = URL::temporarySignedRoute( 22 | 'verification.verify', 23 | now()->addMinutes(60), 24 | ['id' => $user->id, 'hash' => sha1($user->email)] 25 | ); 26 | 27 | $response = $this->actingAs($user)->get($verificationUrl); 28 | 29 | Event::assertDispatched(Verified::class); 30 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 31 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 32 | }); 33 | 34 | test('email is not verified with invalid hash', function () { 35 | $user = User::factory()->unverified()->create(); 36 | 37 | $verificationUrl = URL::temporarySignedRoute( 38 | 'verification.verify', 39 | now()->addMinutes(60), 40 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 41 | ); 42 | 43 | $this->actingAs($user)->get($verificationUrl); 44 | 45 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 31 | $response = $this->get('/reset-password/'.$notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response 55 | ->assertSessionHasNoErrors() 56 | ->assertRedirect(route('login')); 57 | 58 | return true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('correct password must be provided to update password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrorsIn('updatePassword', 'current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | 9 | test('new users can register', function () { 10 | $response = $this->post('/register', [ 11 | 'name' => 'Test User', 12 | 'email' => 'test@example.com', 13 | 'password' => 'password', 14 | 'password_confirmation' => 'password', 15 | ]); 16 | 17 | $this->assertAuthenticated(); 18 | $response->assertRedirect(route('dashboard', absolute: false)); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: ['resources/css/app.css', 'resources/js/app.js'], 8 | refresh: true, 9 | }), 10 | ], 11 | }); 12 | --------------------------------------------------------------------------------