├── public ├── favicon.ico ├── robots.txt ├── img │ ├── ico.png │ ├── logo-big.png │ ├── logo-mid.png │ ├── logo-text.png │ ├── apple-touch-icon.png │ └── default-avatar.png ├── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── fonts │ └── Mona-Sans.woff2 ├── brand │ ├── pinkary-icon.zip │ ├── pinkary-logo-black.zip │ └── pinkary-logo-white.zip ├── index.php ├── js │ └── filament │ │ └── forms │ │ └── components │ │ ├── textarea.js │ │ ├── tags-input.js │ │ └── key-value.js └── .htaccess ├── database ├── backups │ └── .gitkeep ├── .gitignore ├── seeders │ └── DatabaseSeeder.php ├── migrations │ ├── 2024_03_21_023728_add_pinned_to_questions_table.php │ ├── 2024_02_25_114324_add_timezone_to_users_table.php │ ├── 2024_02_21_183041_add_settings_column_to_users_table.php │ ├── 2024_04_06_171145_alter_table_users_remove_timezone_column.php │ ├── 2024_02_22_001030_add_avatar_to_users_table.php │ ├── 2024_04_13_202648_add_views_column_to_users_table.php │ ├── 2024_02_19_210237_add_links_sort_to_users_table.php │ ├── 2024_02_25_175147_add_is_reported_to_questions_table.php │ ├── 2024_07_05_152231_add_parent_id_to_questions.php │ ├── 2024_04_13_202506_add_views_column_to_questions_table.php │ ├── 2024_07_23_055716_add_is_visible_to_links_table.php │ ├── 2024_02_23_203342_add_is_verified_to_users_table.php │ ├── 2024_04_03_201010_add_is_ignored_to_questions_table.php │ ├── 2024_04_06_112143_add_click_count_to_links_table.php │ ├── 2024_04_08_142134_add_avatar_updated_at_to_users_table.php │ ├── 2024_03_01_225200_add_mail_preference_time_to_users_table.php │ ├── 2024_03_24_173834_add_github_username_to_users_table.php │ ├── 2024_04_07_150326_add_is_company_verified_to_users_table.php │ ├── 2024_04_18_124813_add_is_uploaded_avatar_column_to_users_table.php │ ├── 2024_05_12_153034_rename_answered_at_to_answer_created_at_in_questions_table.php │ ├── 2024_03_30_152936_add_anonymously_preference_to_users_table.php │ ├── 2024_07_25_232940_optimize_database_settings.php │ ├── 2024_07_25_183750_set_journal_mode_and_synchronous_settings.php │ ├── 2024_07_25_212051_optimize_database_settings.php │ ├── 2024_02_17_090102_create_links_table.php │ ├── 2024_05_11_141256_add_answer_updated_at_to_questions_table.php │ ├── 2024_02_28_000521_create_notifications_table.php │ ├── 2024_05_01_170027_add_user_id_index_to_links_table.php │ ├── 2024_03_29_040310_create_followers_table.php │ ├── 2024_05_01_152116_add_from_id_and_to_id_indexes_to_questions_table.php │ ├── 2024_02_25_144346_create_likes_table.php │ ├── 2024_07_06_191351_create_bookmarks_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2024_10_13_234344_create_pan_tables.php │ ├── 2024_02_20_213416_create_questions_table.php │ ├── 2024_08_07_203106_create_hashtags_table.php │ └── 2024_02_25_202236_add_uuid_to_questions_table.php └── factories │ ├── Concerns │ └── RefreshOnCreate.php │ ├── HashtagFactory.php │ ├── PanAnalyticFactory.php │ ├── LikeFactory.php │ ├── LinkFactory.php │ └── BookmarkFactory.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── resources ├── js │ ├── bootstrap.js │ ├── abbreviate.js │ ├── copy-url.js │ ├── click-handler.js │ ├── show-more.js │ └── share-profile.js ├── views │ ├── components │ │ ├── section-border.blade.php │ │ ├── input-label.blade.php │ │ ├── autocomplete │ │ │ └── hashtag-item.blade.php │ │ ├── badge.blade.php │ │ ├── icons │ │ │ ├── pin.blade.php │ │ │ ├── close.blade.php │ │ │ ├── plus.blade.php │ │ │ ├── bars.blade.php │ │ │ ├── chevron-left.blade.php │ │ │ ├── bolt.blade.php │ │ │ ├── chart.blade.php │ │ │ ├── magnifying-glass.blade.php │ │ │ ├── paper-airplane.blade.php │ │ │ ├── trending-solid.blade.php │ │ │ ├── bookmark-solid.blade.php │ │ │ ├── bookmark.blade.php │ │ │ ├── arrow-top.blade.php │ │ │ ├── ellipsis-horizontal.blade.php │ │ │ ├── heart.blade.php │ │ │ ├── user.blade.php │ │ │ ├── link.blade.php │ │ │ ├── reset.blade.php │ │ │ ├── twitter-x.blade.php │ │ │ ├── bell.blade.php │ │ │ ├── eye.blade.php │ │ │ ├── chat-bubble.blade.php │ │ │ ├── home.blade.php │ │ │ ├── light-bulb.blade.php │ │ │ ├── share.blade.php │ │ │ ├── verified-company.blade.php │ │ │ ├── verified.blade.php │ │ │ ├── heart-solid.blade.php │ │ │ ├── smile.blade.php │ │ │ ├── sortable-handle.blade.php │ │ │ ├── users.blade.php │ │ │ ├── trash.blade.php │ │ │ ├── eye-off.blade.php │ │ │ ├── camera.blade.php │ │ │ ├── globe.blade.php │ │ │ ├── github.blade.php │ │ │ └── qr-code.blade.php │ │ ├── dropdown-button.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── input-error.blade.php │ │ ├── danger-button.blade.php │ │ ├── primary-colorless-button.blade.php │ │ ├── checkbox.blade.php │ │ ├── secondary-button.blade.php │ │ ├── text-input.blade.php │ │ ├── primary-button.blade.php │ │ ├── load-more-button.blade.php │ │ ├── comments.blade.php │ │ ├── back-to-top.blade.php │ │ ├── image-lightbox.blade.php │ │ ├── follow-button.blade.php │ │ ├── select-input.blade.php │ │ ├── nav-link.blade.php │ │ ├── links │ │ │ └── list-item.blade.php │ │ ├── textarea.blade.php │ │ ├── responsive-nav-link.blade.php │ │ ├── dropdown-link-profile.blade.php │ │ ├── post-divider.blade.php │ │ └── modal-qr-code.blade.php │ ├── vendor │ │ ├── mail │ │ │ └── html │ │ │ │ └── header.blade.php │ │ └── pulse │ │ │ └── dashboard.blade.php │ ├── livewire │ │ ├── navigation │ │ │ └── notifications-count │ │ │ │ └── show.blade.php │ │ ├── bookmarks │ │ │ └── index.blade.php │ │ ├── about-users-avatars.blade.php │ │ ├── flash-messages │ │ │ └── show.blade.php │ │ ├── questions │ │ │ └── index.blade.php │ │ └── home │ │ │ └── trending-questions.blade.php │ ├── bookmarks │ │ └── index.blade.php │ ├── notifications │ │ └── index.blade.php │ ├── home │ │ ├── users.blade.php │ │ ├── trending-questions.blade.php │ │ ├── feed.blade.php │ │ └── following.blade.php │ ├── hashtag │ │ └── show.blade.php │ ├── profile │ │ └── show.blade.php │ ├── mail │ │ └── pending-notifications.blade.php │ ├── support.blade.php │ ├── layouts │ │ └── about.blade.php │ └── auth │ │ └── confirm-password.blade.php └── css │ └── app.css ├── postcss.config.js ├── tests ├── Fixtures │ ├── Turnstile │ │ ├── error.json │ │ └── success.json │ └── TestType.php ├── Arch │ ├── ContractsTest.php │ ├── MailablesTest.php │ ├── ProvidersTest.php │ ├── ViewTest.php │ ├── JobsTest.php │ ├── ConsoleTest.php │ ├── ServicesTest.php │ ├── ExceptionsTest.php │ ├── RulesTest.php │ ├── EnumsTest.php │ ├── NotificationsTest.php │ ├── FactoriesTest.php │ ├── GlobalTest.php │ ├── HttpTest.php │ └── LivewireTest.php ├── .pest │ └── snapshots │ │ └── Unit │ │ └── Services │ │ ├── ContentProvidersTest │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example______http___example__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____www_example_com______www_example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____htt___example_com______htt___example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com______http___example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http__example_com______http__example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http__example_com______http__example_com____2.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____example_com______http____example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____example_com______http____example_com____2.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___exa__mple_com______http___exa__mple_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example__com______http___example__com__.snap │ │ ├── image_exists.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_👍______http___example_com_👍__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_abcd______http___example_com_abcd__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____protocol___example_com______protocol___example_com__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_this___that______http___example_com_this___that__.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_this_that_…__that______http___example_com_this_that______that__.snap │ │ ├── mention_with_data_set_____w31r4_________w31r4____.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____2001_0db8_85a3_0000_0…_7334_______http____2001_0db8_85a3_0000_0____7334___.snap │ │ ├── malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____2001_0db8_85a3_0000_0…__8080______http____2001_0db8_85a3_0000_0_____8080__.snap │ │ ├── mention_with_data_set_____nunomaduro________nunomaduro___.snap │ │ ├── mention_with_data_set_____nunomaduro________nunomaduro_____2.snap │ │ ├── mention_with_data_set_____nunomaduro________nunomaduro_____3.snap │ │ ├── mention_with_data_set_____nunomaduro________nunomaduro_____4.snap │ │ ├── mention_with_data_set_____nunomaduro________nunomaduro_____5.snap │ │ ├── mention_with_data_set____Hi__nunomaduro______Hi__nunomaduro__.snap │ │ ├── mention_with_data_set_____nunomaduro_hi_______nunomaduro_hi__.snap │ │ ├── code_with_data_set_______phpecho__Hello__World_______________php_necho__Hello__World____n_____.snap │ │ ├── code_with_data_set_______php_echo__Hello__World_______________php__necho__Hello__World____n_____.snap │ │ └── code_with_data_set____Check_this____echo__Hello…__________Check_this__n____necho__Hello_____n_____.snap │ │ └── ContentTest │ │ ├── it_ignores_mention_inside__a_.snap │ │ ├── link.snap │ │ └── mention.snap ├── Http │ ├── ChangelogTest.php │ ├── Citadel │ │ ├── LoginTest.php │ │ ├── DashboardTest.php │ │ ├── Questions │ │ │ └── IndexTest.php │ │ └── Users │ │ │ └── IndexTest.php │ ├── TelegramTest.php │ ├── Home │ │ ├── UsersTest.php │ │ ├── TrendingTest.php │ │ └── FollowingTest.php │ ├── LogoutTest.php │ ├── Profile │ │ ├── Connect │ │ │ └── GitHub │ │ │ │ └── IndexTest.php │ │ └── Timezone │ │ │ └── UpdateTest.php │ ├── Bookmarks │ │ └── IndexTest.php │ ├── Hashtag │ │ └── ShowTest.php │ ├── VerificationNotificationTest.php │ └── ConfirmPasswordTest.php ├── TestCase.php ├── Unit │ ├── Factories │ │ └── HashtagFactoryTest.php │ ├── Livewire │ │ ├── AboutUsersAvatarsTest.php │ │ └── Views │ │ │ └── CreateTest.php │ ├── Notifications │ │ ├── QuestionAnsweredTest.php │ │ ├── QuestionCreatedTest.php │ │ └── UserMentionedTest.php │ ├── Policies │ │ ├── LikePolicyTest.php │ │ ├── LinkPolicyTest.php │ │ ├── BookmarkPolicyTest.php │ │ └── UserPolicyTest.php │ ├── Services │ │ ├── FirewallTest.php │ │ ├── ChangelogTest.php │ │ ├── BioTest.php │ │ └── AvatarTest.php │ ├── Models │ │ ├── HashtagTest.php │ │ ├── LinkTest.php │ │ ├── LikeTest.php │ │ └── BookmarkTest.php │ ├── Citadel │ │ ├── Users │ │ │ └── IndexTest.php │ │ └── Questions │ │ │ └── QuestionOverviewTest.php │ ├── Rules │ │ ├── ValidTimezoneTest.php │ │ ├── RecaptchaTest.php │ │ └── MaxUploadsTest.php │ └── Mail │ │ └── PendingNotificationsTest.php └── Console │ ├── SyncVerifiedUsersCommandTest.php │ └── PerformDatabaseBackupCommandTest.php ├── app ├── Exceptions │ └── GitHubException.php ├── Contracts │ ├── Services │ │ └── ParsableContentProvider.php │ └── Models │ │ └── Viewable.php ├── Http │ ├── Controllers │ │ ├── BookmarksController.php │ │ ├── HashtagController.php │ │ ├── ChangelogController.php │ │ ├── UserTimezoneController.php │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ └── UpdatePasswordController.php │ │ ├── UserIsVerifiedController.php │ │ ├── QuestionController.php │ │ └── NotificationController.php │ ├── Middleware │ │ └── EnsureVerifiedEmailsForSignInUsers.php │ └── Requests │ │ └── UserAvatarUpdateRequest.php ├── Livewire │ ├── FlashMessages │ │ └── Show.php │ ├── Concerns │ │ ├── HasLoadMore.php │ │ └── NeedsVerifiedEmail.php │ ├── Home │ │ ├── TrendingQuestions.php │ │ └── QuestionsFollowing.php │ ├── AboutUsersAvatars.php │ ├── Views │ │ └── Create.php │ ├── Navigation │ │ └── NotificationsCount │ │ │ └── Show.php │ └── Bookmarks │ │ └── Index.php ├── Policies │ ├── LikePolicy.php │ ├── BookmarkPolicy.php │ ├── UserPolicy.php │ └── LinkPolicy.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ ├── AboutLayout.php │ │ ├── GuestLayout.php │ │ └── Footer.php ├── Filament │ └── Resources │ │ ├── UserResource │ │ └── Pages │ │ │ └── Index.php │ │ └── QuestionResource │ │ └── Pages │ │ └── Index.php ├── Services │ ├── Git.php │ ├── ParsableContentProviders │ │ ├── StripProviderParsable.php │ │ ├── BrProviderParsable.php │ │ ├── MentionProviderParsable.php │ │ └── HashtagProviderParsable.php │ ├── Autocomplete │ │ └── Result.php │ ├── Firewall.php │ ├── ParsableBio.php │ └── Avatar.php ├── Providers │ ├── GitHubServiceProvider.php │ └── PulseServiceProvider.php ├── Enums │ └── UserMailPreference.php ├── Models │ └── PanAnalytic.php ├── Rules │ ├── MaxUploads.php │ ├── VerifiedOnly.php │ └── NoBlankCharacters.php ├── Jobs │ └── SyncVerifiedUser.php ├── Console │ └── Commands │ │ └── SyncVerifiedUsersCommand.php └── Notifications │ ├── QuestionAnswered.php │ ├── UserMentioned.php │ └── QuestionCreated.php ├── .gitattributes ├── phpstan.neon ├── artisan ├── .editorconfig ├── vite.config.js ├── .gitignore ├── package.json ├── rector.php ├── routes └── console.php ├── docker-compose.yml ├── forge-deployment.sh └── config └── sponsors.php /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/backups/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | window.axios = axios; 4 | -------------------------------------------------------------------------------- /public/img/ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/ico.png -------------------------------------------------------------------------------- /public/img/logo-big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/logo-big.png -------------------------------------------------------------------------------- /public/img/logo-mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/logo-mid.png -------------------------------------------------------------------------------- /public/img/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/logo-text.png -------------------------------------------------------------------------------- /public/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/screenshots/1.png -------------------------------------------------------------------------------- /public/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/screenshots/2.png -------------------------------------------------------------------------------- /public/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/screenshots/3.png -------------------------------------------------------------------------------- /public/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/screenshots/4.png -------------------------------------------------------------------------------- /public/fonts/Mona-Sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/fonts/Mona-Sans.woff2 -------------------------------------------------------------------------------- /public/brand/pinkary-icon.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/brand/pinkary-icon.zip -------------------------------------------------------------------------------- /public/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/img/default-avatar.png -------------------------------------------------------------------------------- /public/brand/pinkary-logo-black.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/brand/pinkary-logo-black.zip -------------------------------------------------------------------------------- /public/brand/pinkary-logo-white.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobryk0999/Pinkary_1/HEAD/public/brand/pinkary-logo-white.zip -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/Fixtures/Turnstile/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "error-codes": [ 4 | "timeout-or-duplicate" 5 | ], 6 | "messages": [] 7 | } -------------------------------------------------------------------------------- /resources/views/components/section-border.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /tests/Arch/ContractsTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Contracts') 7 | ->toBeInterfaces(); 8 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example______http___example__.snap: -------------------------------------------------------------------------------- 1 | http://example -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____www_example_com______www_example_com__.snap: -------------------------------------------------------------------------------- 1 | www.example.com -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____htt___example_com______htt___example_com__.snap: -------------------------------------------------------------------------------- 1 | htt://example.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com______http___example_com__.snap: -------------------------------------------------------------------------------- 1 | http://example=com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http__example_com______http__example_com__.snap: -------------------------------------------------------------------------------- 1 | http//example.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http__example_com______http__example_com____2.snap: -------------------------------------------------------------------------------- 1 | http:/example.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____example_com______http____example_com__.snap: -------------------------------------------------------------------------------- 1 | http://.example.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____example_com______http____example_com____2.snap: -------------------------------------------------------------------------------- 1 | http:///example.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___exa__mple_com______http___exa__mple_com__.snap: -------------------------------------------------------------------------------- 1 | http://exa_mple.com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example__com______http___example__com__.snap: -------------------------------------------------------------------------------- 1 | http://example..com -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/image_exists.snap: -------------------------------------------------------------------------------- 1 | image -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_👍______http___example_com_👍__.snap: -------------------------------------------------------------------------------- 1 | http://example.com/👍 -------------------------------------------------------------------------------- /tests/Arch/MailablesTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Mail') 7 | ->toHaveConstructor() 8 | ->toExtend('Illuminate\Mail\Mailable'); 9 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_abcd______http___example_com_abcd__.snap: -------------------------------------------------------------------------------- 1 | http://example.com:abcd -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____protocol___example_com______protocol___example_com__.snap: -------------------------------------------------------------------------------- 1 | protocol://example.com -------------------------------------------------------------------------------- /tests/Arch/ProvidersTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Providers') 7 | ->toExtend('Illuminate\Support\ServiceProvider') 8 | ->not->toBeUsed(); 9 | -------------------------------------------------------------------------------- /resources/views/components/input-label.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'value', 3 | ]) 4 | 5 | 8 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http___example_com_this___that______http___example_com_this___that__.snap: -------------------------------------------------------------------------------- 1 | http://example.com?this<>=that -------------------------------------------------------------------------------- /app/Exceptions/GitHubException.php: -------------------------------------------------------------------------------- 1 | get('/changelog') 7 | ->assertOk() 8 | ->assertViewIs('changelog'); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/Http/Citadel/LoginTest.php: -------------------------------------------------------------------------------- 1 | get('citadel/login'); 7 | 8 | $response->assertStatus(404); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | =that -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____w31r4_________w31r4____.snap: -------------------------------------------------------------------------------- 1 | @w31r4_ -------------------------------------------------------------------------------- /tests/Arch/ViewTest.php: -------------------------------------------------------------------------------- 1 | expect('App\View\Components') 7 | ->toExtend('Illuminate\View\Component') 8 | ->toHaveMethod('render') 9 | ->not->toBeUsed(); 10 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____2001_0db8_85a3_0000_0…_7334_______http____2001_0db8_85a3_0000_0____7334___.snap: -------------------------------------------------------------------------------- 1 | http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334] -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentTest/it_ignores_mention_inside__a_.snap: -------------------------------------------------------------------------------- 1 | pinkary.com/@nunomaduro -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['url']) 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/malformed_links_are_correctly_handled_by_content_parser_with_data_set____http____2001_0db8_85a3_0000_0…__8080______http____2001_0db8_85a3_0000_0_____8080__.snap: -------------------------------------------------------------------------------- 1 | http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080 -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro________nunomaduro___.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro. -------------------------------------------------------------------------------- /tests/Http/TelegramTest.php: -------------------------------------------------------------------------------- 1 | get(route('telegram')); 7 | 8 | $response->assertRedirect('https://t.me/+Yv9CUTC1q29lNzg8'); 9 | }); 10 | -------------------------------------------------------------------------------- /resources/views/components/autocomplete/hashtag-item.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \App\Services\Autocomplete\Result $result */ 3 | @endphp 4 | 5 | {{ $result->replacement }} 6 | 7 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro________nunomaduro_____2.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro, -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro________nunomaduro_____3.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro! -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro________nunomaduro_____4.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro? -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro________nunomaduro_____5.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro/ -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set____Hi__nunomaduro______Hi__nunomaduro__.snap: -------------------------------------------------------------------------------- 1 | Hi @nunomaduro -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/mention_with_data_set_____nunomaduro_hi_______nunomaduro_hi__.snap: -------------------------------------------------------------------------------- 1 | @nunomaduro hi -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentTest/link.snap: -------------------------------------------------------------------------------- 1 | Sure, here is the link: example.com. Let me know if you have any questions. -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | expect('App\Jobs') 7 | ->toHaveMethod('handle') 8 | ->toHaveConstructor() 9 | ->toExtendNothing() 10 | ->toImplement('Illuminate\Contracts\Queue\ShouldQueue'); 11 | -------------------------------------------------------------------------------- /tests/Fixtures/Turnstile/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "error-codes": [], 4 | "challenge_ts": "2023-09-11T14:40:06.952Z", 5 | "hostname": "turnstile-test.test", 6 | "action": "", 7 | "cdata": "", 8 | "metadata": { 9 | "interactive": true 10 | } 11 | } -------------------------------------------------------------------------------- /resources/views/components/badge.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $classes = "inline-block px-2 py-1 leading-none bg-slate-900 text-xs text-slate-50 rounded-full shadow-md"; 3 | @endphp 4 | 5 | merge(['class' => $classes]) }} 7 | > 8 | {{ $slot }} 9 | 10 | -------------------------------------------------------------------------------- /resources/views/components/icons/pin.blade.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 4 | 5 | parameters: 6 | level: max 7 | 8 | paths: 9 | - app 10 | - config 11 | - bootstrap 12 | - database/factories 13 | - routes 14 | -------------------------------------------------------------------------------- /tests/Arch/ConsoleTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Console\Commands') 7 | ->toExtend('Illuminate\Console\Command') 8 | ->toHaveSuffix('Command') 9 | ->toHaveMethod('handle') 10 | ->toImplementNothing() 11 | ->not->toBeUsed(); 12 | -------------------------------------------------------------------------------- /tests/Arch/ServicesTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Services\ParsableContentProviders') 7 | ->toImplement('App\Contracts\Services\ParsableContentProvider') 8 | ->toOnlyBeUsedIn([ 9 | 'App\Services', 10 | ]); 11 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 11 | 12 | exit($status); 13 | -------------------------------------------------------------------------------- /resources/views/livewire/navigation/notifications-count/show.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if ($count > 0) 3 | 4 | {{ $count > 20 ? '20+' : $count }} 5 | 6 | @endif 7 |
8 | -------------------------------------------------------------------------------- /app/Contracts/Services/ParsableContentProvider.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 dark:text-slate-400 text-slate-600 dark:hover:bg-slate-800 hover:bg-slate-200 transition duration-150 ease-in-out']) }} 3 | wire:navigate 4 | > 5 | {{ $slot }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/close.blade.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/views/components/icons/plus.blade.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/views/components/icons/bars.blade.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/code_with_data_set_______phpecho__Hello__World_______________php_necho__Hello__World____n_____.snap: -------------------------------------------------------------------------------- 1 |
echo "Hello, World!";
-------------------------------------------------------------------------------- /resources/views/components/icons/chevron-left.blade.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/code_with_data_set_______php_echo__Hello__World_______________php__necho__Hello__World____n_____.snap: -------------------------------------------------------------------------------- 1 |
echo "Hello, World!";
-------------------------------------------------------------------------------- /resources/views/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'messages', 3 | ]) 4 | 5 | @if ($messages) 6 | 11 | @endif 12 | -------------------------------------------------------------------------------- /tests/Arch/ExceptionsTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Exceptions') 7 | ->toImplement('Throwable') 8 | ->toOnlyBeUsedIn([ 9 | 'App\Console\Commands', 10 | 'App\Http\Controllers', 11 | 'App\Livewire', 12 | 'App\Services', 13 | ]); 14 | -------------------------------------------------------------------------------- /tests/Http/Home/UsersTest.php: -------------------------------------------------------------------------------- 1 | get(route('home.users')); 9 | 10 | $response->assertOk() 11 | ->assertSee('Search') 12 | ->assertSeeLivewire(Users::class); 13 | }); 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/Contracts/Models/Viewable.php: -------------------------------------------------------------------------------- 1 | $ids 13 | */ 14 | public static function incrementViews(array $ids): void; 15 | } 16 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | 2 | Bookmarks 3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 | -------------------------------------------------------------------------------- /tests/Arch/RulesTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Rules') 7 | ->toExtendNothing() 8 | ->toImplement('Illuminate\Contracts\Validation\ValidationRule') 9 | ->toOnlyBeUsedIn([ 10 | 'App\Http\Controllers', 11 | 'App\Http\Requests', 12 | 'App\Livewire', 13 | ]); 14 | -------------------------------------------------------------------------------- /tests/Unit/Factories/HashtagFactoryTest.php: -------------------------------------------------------------------------------- 1 | create(); 9 | 10 | $this->assertCount(40, $hashtags); 11 | $this->assertEquals(40, $hashtags->unique('name')->count()); 12 | }); 13 | -------------------------------------------------------------------------------- /resources/views/notifications/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | Notifications 3 | 4 |
5 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /resources/views/components/danger-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/icons/bolt.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/primary-colorless-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/Http/Home/TrendingTest.php: -------------------------------------------------------------------------------- 1 | get(route('home.trending')); 9 | 10 | $response->assertOk() 11 | ->assertSee('Trending') 12 | ->assertSeeLivewire(TrendingQuestions::class); 13 | }); 14 | -------------------------------------------------------------------------------- /resources/views/components/checkbox.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'focus:ring-pink-500 dark:focus:ring-offset-slate-900 focus:ring-offset-slate-200 size-4.5 text-pink-500 dark:border-slate-800 border-slate-200 dark:bg-slate-900 bg-slate-50 dark:ring-slate-900 ring-slate-200 rounded cursor-pointer dark:shadow-none shadow-md']) !!} 4 | /> 5 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/AboutUsersAvatarsTest.php: -------------------------------------------------------------------------------- 1 | create(); 11 | 12 | $component = Livewire::test(AboutUsersAvatars::class); 13 | 14 | $component->assertOk(); 15 | }); 16 | -------------------------------------------------------------------------------- /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: [ 8 | 'resources/css/app.css', 9 | 'resources/js/app.js', 10 | ], 11 | refresh: true, 12 | }), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /resources/views/components/icons/chart.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/home/users.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /app/Http/Controllers/BookmarksController.php: -------------------------------------------------------------------------------- 1 | @nunomaduro, let me know if you have any questions. Thanks @xiCO2k. -------------------------------------------------------------------------------- /tests/Arch/EnumsTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Enums') 7 | ->toBeEnums() 8 | ->toExtendNothing() 9 | ->toUseNothing() 10 | ->toHaveMethod('toArray') 11 | ->toOnlyBeUsedIn([ 12 | 'App\Console\Commands', 13 | 'App\Http\Requests', 14 | 'App\Livewire', 15 | 'App\Models', 16 | ]); 17 | -------------------------------------------------------------------------------- /resources/views/components/icons/magnifying-glass.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/home/trending-questions.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | package-lock.json 18 | /.fleet 19 | /.idea 20 | /.vscode 21 | /database/backups/*.sql 22 | debugbar 23 | .DS_Store -------------------------------------------------------------------------------- /app/Livewire/FlashMessages/Show.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 16 | -------------------------------------------------------------------------------- /resources/views/components/icons/paper-airplane.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Arch/NotificationsTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Notifications') 7 | ->toHaveConstructor() 8 | ->toExtend('Illuminate\Notifications\Notification') 9 | ->toOnlyBeUsedIn([ 10 | 'App\Console\Commands', 11 | 'App\Http\Controllers', 12 | 'App\Observers', 13 | 'App\Livewire\Notifications\Index', 14 | ]); 15 | -------------------------------------------------------------------------------- /resources/views/components/icons/trending-solid.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Http/Citadel/DashboardTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create([ 10 | 'email' => 'enunomaduro@gmail.com', 11 | ])) 12 | ->get('citadel/') 13 | ->assertSeeLivewire(Analytics::class) 14 | ->assertStatus(200); 15 | }); 16 | -------------------------------------------------------------------------------- /app/Policies/LikePolicy.php: -------------------------------------------------------------------------------- 1 | id === $like->user_id; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/components/icons/bookmark-solid.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/components/text-input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'dark:text-white text-black dark:caret-white caret-black focus:border-pink-500 dark:border-slate-800 border-slate-300 dark:bg-slate-900/50 bg-slate-50/20 backdrop-blur-sm dark:focus:ring-slate-900 focus:ring-slate-100 rounded-lg shadow-sm sm:text-sm']) !!} 6 | /> 7 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/QuestionAnsweredTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | $notification = new QuestionAnswered($question); 12 | 13 | expect($notification->toDatabase($question->to))->toBe(['question_id' => $question->id]); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/QuestionCreatedTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | $notification = new QuestionCreated($question); 12 | 13 | expect($notification->toDatabase($question->to))->toBe(['question_id' => $question->id]); 14 | }); 15 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | { 2 | const SI_SYMBOL = ["", "K", "M", "B", "T"]; 3 | const tier = (Math.log10(Math.abs(number)) / 3) | 0; 4 | if (tier === 0) return number; 5 | const suffix = SI_SYMBOL[tier]; 6 | const scale = Math.pow(10, tier * 3); 7 | const scaled = number / scale; 8 | return scaled.toFixed(precision) + suffix; 9 | } 10 | 11 | export { abbreviate }; 12 | -------------------------------------------------------------------------------- /app/View/Components/AboutLayout.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/Filament/Resources/UserResource/Pages/Index.php: -------------------------------------------------------------------------------- 1 | id === $bookmark->user_id; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/components/primary-button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'as' => 'button', 3 | ]) 4 | 5 | <{{ $as }} {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 border text-pink-500 border-pink-500 rounded-lg font-semibold text-xs tracking-widest dark:hover:bg-pink-900/20 hover:bg-pink-400/20 focus:outline-none focus:ring-0 focus:ring-offset-0 transition ease-in-out duration-150']) }}> 6 | {{ $slot }} 7 | 8 | -------------------------------------------------------------------------------- /app/Http/Controllers/HashtagController.php: -------------------------------------------------------------------------------- 1 | $hashtag, 18 | ]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/arrow-top.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/components/icons/ellipsis-horizontal.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/heart.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/user.blade.php: -------------------------------------------------------------------------------- 1 | merge(['stroke-width' => '1.5']) }} 7 | > 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/Arch/FactoriesTest.php: -------------------------------------------------------------------------------- 1 | expect('Database\Factories') 7 | ->toExtend('Illuminate\Database\Eloquent\Factories\Factory') 8 | ->ignoring('Database\Factories\Concerns') 9 | ->toUse('Database\Factories\Concerns\RefreshOnCreate') 10 | ->toHaveMethod('definition') 11 | ->ignoring('Database\Factories\Concerns') 12 | ->toOnlyBeUsedIn([ 13 | 'App\Models', 14 | ]); 15 | -------------------------------------------------------------------------------- /tests/Unit/Policies/LikePolicyTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | $like = Like::factory()->create(['user_id' => $user->id]); 11 | 12 | expect($user->can('delete', $like))->toBeTrue(); 13 | 14 | $user = User::factory()->create(); 15 | 16 | expect($user->can('delete', $like))->toBeFalse(); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/Unit/Policies/LinkPolicyTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | $link = Link::factory()->create(['user_id' => $user->id]); 11 | 12 | expect($user->can('delete', $link))->toBeTrue(); 13 | 14 | $user = User::factory()->create(); 15 | 16 | expect($user->can('delete', $link))->toBeFalse(); 17 | }); 18 | -------------------------------------------------------------------------------- /app/Services/Git.php: -------------------------------------------------------------------------------- 1 | run(['git', 'describe', '--tags', '--abbrev=0'])->output(), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/link.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/reset.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/home/feed.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | @auth 7 | 8 | @endauth 9 | 10 | 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /app/Services/ParsableContentProviders/StripProviderParsable.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/views/hashtag/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | Recent posts with #{{ $hashtag }} 3 | 4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /tests/Unit/Policies/BookmarkPolicyTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | $bookmark = Bookmark::factory()->create(['user_id' => $user->id]); 11 | 12 | expect($user->can('delete', $bookmark))->toBeTrue(); 13 | 14 | $user = User::factory()->create(); 15 | 16 | expect($user->can('delete', $bookmark))->toBeFalse(); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/.pest/snapshots/Unit/Services/ContentProvidersTest/code_with_data_set____Check_this____echo__Hello…__________Check_this__n____necho__Hello_____n_____.snap: -------------------------------------------------------------------------------- 1 | Check this: 2 |
echo "Hello, World!";
3 | 4 | and this: 5 |
echo "Hello, World!";
-------------------------------------------------------------------------------- /resources/views/profile/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /tests/Arch/GlobalTest.php: -------------------------------------------------------------------------------- 1 | expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep', 'dispatch', 'dispatch_sync']) 7 | ->not->toBeUsed(); 8 | 9 | arch('http helpers') 10 | ->expect(['session', 'auth', 'request']) 11 | ->toOnlyBeUsedIn([ 12 | 'App\Http', 13 | 'App\Rules', 14 | 'App\Livewire', 15 | 'App\Jobs\IncrementViews', 16 | 'App\Services\Autocomplete\Types', 17 | ]); 18 | -------------------------------------------------------------------------------- /app/Services/ParsableContentProviders/BrProviderParsable.php: -------------------------------------------------------------------------------- 1 | ', $content); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/components/load-more-button.blade.php: -------------------------------------------------------------------------------- 1 | @if ($perPage < 100 && $paginator->hasMorePages()) 2 |
3 |
4 | 5 |
6 |
7 | @elseif ($perPage > 10) 8 |
{{ $message }}
9 | @endif 10 | -------------------------------------------------------------------------------- /app/Providers/GitHubServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(GitHub::class, fn (): GitHub => new GitHub(config()->string('services.github.token'))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/components/icons/bell.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /database/migrations/2024_03_21_023728_add_pinned_to_questions_table.php: -------------------------------------------------------------------------------- 1 | boolean('pinned')->default(false)->after('to_id'); 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /resources/views/components/icons/eye.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/Livewire/Concerns/HasLoadMore.php: -------------------------------------------------------------------------------- 1 | perPage = $this->perPage > 100 ? 100 : ($this->perPage + 5); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Services/Autocomplete/Result.php: -------------------------------------------------------------------------------- 1 | $payload 13 | */ 14 | public function __construct( 15 | public string|int $id, 16 | public string $replacement, 17 | public string $view, 18 | public array $payload = [], 19 | ) { 20 | // 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/views/components/icons/chat-bubble.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/home.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/vendor/pulse/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/js/copy-url.js: -------------------------------------------------------------------------------- 1 | const copyUrl = () => ({ 2 | 3 | isVisible: false, 4 | 5 | init() { 6 | if (!navigator.share) { 7 | this.isVisible = true 8 | } 9 | }, 10 | 11 | copyToClipboard(url) { 12 | this.$clipboard(url) 13 | 14 | this.$notify('Copied to clipboard.', { 15 | wrapperId: 'flashMessageWrapper', 16 | templateId: 'flashMessageTemplate', 17 | autoClose: 3000, 18 | autoRemove: 4000 19 | }) 20 | } 21 | }) 22 | 23 | export { copyUrl } 24 | -------------------------------------------------------------------------------- /resources/views/components/comments.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'question' => null, 3 | ]) 4 | 5 | @php 6 | $question->loadMissing('children'); 7 | @endphp 8 | 9 | @if($question->children->isNotEmpty()) 10 |
11 | @foreach($question->children as $comment) 12 | @break($loop->depth > 5) 13 | 14 | 15 | 16 | 17 | @endforeach 18 |
19 | @endif 20 | -------------------------------------------------------------------------------- /resources/views/components/icons/light-bulb.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /database/migrations/2024_02_25_114324_add_timezone_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('timezone')->nullable(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Arch/HttpTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Http\Controllers') 7 | ->toExtendNothing() 8 | ->not->toBeUsed(); 9 | 10 | arch('middleware') 11 | ->expect('App\Http\Middleware') 12 | ->toHaveMethod('handle') 13 | ->toUse('Illuminate\Http\Request') 14 | ->not->toBeUsed(); 15 | 16 | arch('requests') 17 | ->expect('App\Http\Requests') 18 | ->toExtend('Illuminate\Foundation\Http\FormRequest') 19 | ->toHaveMethod('rules') 20 | ->toBeUsedIn('App\Http\Controllers'); 21 | -------------------------------------------------------------------------------- /tests/Unit/Services/FirewallTest.php: -------------------------------------------------------------------------------- 1 | isBot($request))->toBeFalse(); 13 | 14 | request()->server->set('HTTP_USER_AGENT', 'Googlebot/2.1 (+http://www.google.com/bot.html)'); 15 | request()->headers->set('User-Agent', 'Googlebot/2.1 (+http://www.google.com/bot.html)'); 16 | 17 | expect($firewall->isBot($request))->toBeTrue(); 18 | }); 19 | -------------------------------------------------------------------------------- /database/migrations/2024_02_21_183041_add_settings_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | json('settings')->nullable(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_06_171145_alter_table_users_remove_timezone_column.php: -------------------------------------------------------------------------------- 1 | dropColumn('timezone'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/share.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/verified-company.blade.php: -------------------------------------------------------------------------------- 1 | @props(['color']) 2 | merge(['class' => 'fill-current text-yellow-500']) }} aria-label="Verified" role="img" viewBox="0 0 40 40">Verified 4 | -------------------------------------------------------------------------------- /database/migrations/2024_02_22_001030_add_avatar_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('avatar')->nullable()->after('email'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_13_202648_add_views_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('views')->default(0); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/verified.blade.php: -------------------------------------------------------------------------------- 1 | @props(['color']) 2 | merge(['class' => "fill-current saturate-200 text-{$color}"]) }} aria-label="Verified" role="img" viewBox="0 0 40 40">Verified 4 | -------------------------------------------------------------------------------- /database/migrations/2024_02_19_210237_add_links_sort_to_users_table.php: -------------------------------------------------------------------------------- 1 | json('links_sort')->nullable()->after('email'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_02_25_175147_add_is_reported_to_questions_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_reported')->default(false); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_07_05_152231_add_parent_id_to_questions.php: -------------------------------------------------------------------------------- 1 | foreignId('parent_id')->nullable()->after('id'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_13_202506_add_views_column_to_questions_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('views')->default(0); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/heart-solid.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /database/migrations/2024_07_23_055716_add_is_visible_to_links_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_visible')->default(true)->after('click_count'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Http/LogoutTest.php: -------------------------------------------------------------------------------- 1 | create(); 9 | 10 | $response = $this->actingAs($user)->post('/logout'); 11 | 12 | $this->assertGuest(); 13 | 14 | $response->assertRedirect('/'); 15 | }); 16 | 17 | test('users can only logout when authenticated', function () { 18 | $this->assertGuest(); 19 | 20 | $response = $this->post('/logout'); 21 | 22 | $this->assertGuest(); 23 | 24 | $response->assertRedirect('/login'); 25 | }); 26 | -------------------------------------------------------------------------------- /database/migrations/2024_02_23_203342_add_is_verified_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_verified')->default(false)->after('remember_token'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_03_201010_add_is_ignored_to_questions_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_ignored')->default(false)->after('anonymously'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_06_112143_add_click_count_to_links_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('click_count')->default(0)->after('user_id'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_08_142134_add_avatar_updated_at_to_users_table.php: -------------------------------------------------------------------------------- 1 | timestamp('avatar_updated_at')->nullable()->after('avatar'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /resources/views/components/icons/smile.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/icons/sortable-handle.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'size-3']) 2 | 3 | merge(['class' => $class]) }}> 10 | -------------------------------------------------------------------------------- /tests/Fixtures/TestType.php: -------------------------------------------------------------------------------- 1 | string('mail_preference_time')->default('daily')->after('timezone'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_03_24_173834_add_github_username_to_users_table.php: -------------------------------------------------------------------------------- 1 | string('github_username')->nullable()->unique()->after('is_verified'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_04_07_150326_add_is_company_verified_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_company_verified')->default(false)->after('is_verified'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Unit/Notifications/UserMentionedTest.php: -------------------------------------------------------------------------------- 1 | create([ 11 | 'username' => 'johndoe', 12 | ]); 13 | 14 | $question = Question::factory()->create([ 15 | 'content' => 'Hello @johndoe! How are you doing?', 16 | ]); 17 | 18 | $notification = new UserMentioned($question); 19 | 20 | expect($notification->toDatabase($user))->toBe(['question_id' => $question->id]); 21 | }); 22 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 11 | web: __DIR__.'/../routes/web.php', 12 | commands: __DIR__.'/../routes/console.php', 13 | ) 14 | ->withMiddleware(function (Middleware $middleware): void { 15 | // 16 | }) 17 | ->withExceptions(function (Exceptions $exceptions): void { 18 | // 19 | })->create(); 20 | -------------------------------------------------------------------------------- /database/migrations/2024_04_18_124813_add_is_uploaded_avatar_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_uploaded_avatar')->default(false)->avatar_updated_at(); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2024_05_12_153034_rename_answered_at_to_answer_created_at_in_questions_table.php: -------------------------------------------------------------------------------- 1 | renameColumn('answered_at', 'answer_created_at'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Http/Profile/Connect/GitHub/IndexTest.php: -------------------------------------------------------------------------------- 1 | get(route('profile.connect.github')); 9 | 10 | $response->assertStatus(302) 11 | ->assertRedirect(route('login')); 12 | }); 13 | 14 | test('redirect to github', function () { 15 | $response = $this->actingAs(User::factory()->create()) 16 | ->get(route('profile.connect.github')); 17 | 18 | $response->assertStatus(302); 19 | $response->assertRedirectContains('https://github.com/login/oauth/authorize'); 20 | }); 21 | -------------------------------------------------------------------------------- /app/Enums/UserMailPreference.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public static function toArray(): array 19 | { 20 | return [ 21 | self::Daily->value => 'Daily', 22 | self::Weekly->value => 'Weekly', 23 | self::Never->value => 'Never', 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Policies/UserPolicy.php: -------------------------------------------------------------------------------- 1 | id !== $target->id; 17 | } 18 | 19 | /** 20 | * Determine whether the user can unfollow the user. 21 | */ 22 | public function unfollow(User $user, User $target): bool 23 | { 24 | return $user->id !== $target->id; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2024_03_30_152936_add_anonymously_preference_to_users_table.php: -------------------------------------------------------------------------------- 1 | boolean('prefers_anonymous_questions')->default(true)->after('settings'); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Unit/Models/HashtagTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | expect(array_keys($question->toArray()))->toBe([ 12 | 'id', 13 | 'name', 14 | 'created_at', 15 | 'updated_at', 16 | ]); 17 | }); 18 | 19 | test('relations', function () { 20 | $hashtag = Hashtag::factory() 21 | ->hasQuestions(1) 22 | ->create(); 23 | 24 | expect($hashtag->questions)->each->toBeInstanceOf(Question::class); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/Unit/Services/ChangelogTest.php: -------------------------------------------------------------------------------- 1 | getReleases(); 11 | 12 | expect($releases)->toBeArray(); 13 | 14 | foreach ($releases as $version => $release) { 15 | expect($version)->toBeString() 16 | ->and($release)->toBeArray() 17 | ->and($release)->toHaveKey('publishedAt') 18 | ->and($release)->toHaveKey('changes') 19 | ->and($release['changes'])->toBeArray(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /resources/views/components/back-to-top.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'offset' => '800', 3 | ]) 4 | 5 |
11 | 19 |
20 | -------------------------------------------------------------------------------- /resources/views/mail/pending-notifications.blade.php: -------------------------------------------------------------------------------- 1 | 2 | # Hello, {{ $user->name }}! 3 | 4 | We've noticed you have {{ $pendingNotificationsCount }} {{ Str::plural('notification', $pendingNotificationsCount) }}. You can view notifications by clicking the button below. 5 | 6 | 7 | View Notifications 8 | 9 | 10 | If you no longer wish to receive these emails, you can change your "Mail Preference Time" in your [profile settings]({{ route('profile.edit') }}). 11 | 12 | See you soon,
13 | {{ config('app.name') }} 14 | 15 |
16 | -------------------------------------------------------------------------------- /tests/Http/Bookmarks/IndexTest.php: -------------------------------------------------------------------------------- 1 | get(route('bookmarks.index')); 10 | 11 | $response->assertRedirect(route('login')); 12 | }); 13 | 14 | test('auth', function () { 15 | $user = User::factory()->create(); 16 | 17 | $response = $this->actingAs($user) 18 | ->get(route('bookmarks.index')) 19 | ->assertStatus(200); 20 | 21 | $response->assertOk() 22 | ->assertSee('Bookmarks') 23 | ->assertSeeLivewire(Index::class); 24 | }); 25 | -------------------------------------------------------------------------------- /app/Services/Firewall.php: -------------------------------------------------------------------------------- 1 | userAgent(); 18 | 19 | $botParser = new BotParser(); 20 | $botParser->setUserAgent($userAgent); 21 | $botParser->discardDetails(); 22 | 23 | return ! is_null($botParser->parse()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Console/SyncVerifiedUsersCommandTest.php: -------------------------------------------------------------------------------- 1 | create(); 12 | 13 | User::factory(2)->create([ 14 | 'is_verified' => true, 15 | ]); 16 | 17 | Queue::fake(SyncVerifiedUser::class); 18 | 19 | $this->artisan(SyncVerifiedUsersCommand::class) 20 | ->assertExitCode(0); 21 | 22 | Queue::assertPushed(SyncVerifiedUser::class, 2); 23 | }); 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/ChangelogController.php: -------------------------------------------------------------------------------- 1 | Cache::remember( 20 | 'changelog.releases', 120, fn (): array => $changelog->getReleases(), 21 | ), 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Policies/LinkPolicy.php: -------------------------------------------------------------------------------- 1 | id === $link->user_id; 18 | } 19 | 20 | /** 21 | * Determine whether the user can update the link. 22 | */ 23 | public function update(User $user, Link $link): bool 24 | { 25 | return $user->id === $link->user_id; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Models/PanAnalytic.php: -------------------------------------------------------------------------------- 1 | */ 19 | use HasFactory; 20 | 21 | /** 22 | * Indicates if the model should be timestamped. 23 | * 24 | * @var bool 25 | */ 26 | public $timestamps = false; 27 | } 28 | -------------------------------------------------------------------------------- /tests/Unit/Models/LinkTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 10 | 11 | expect(array_keys($question->toArray()))->toBe([ 12 | 'id', 13 | 'description', 14 | 'url', 15 | 'user_id', 16 | 'created_at', 17 | 'updated_at', 18 | 'click_count', 19 | 'is_visible', 20 | ]); 21 | }); 22 | 23 | test('relations', function () { 24 | $link = Link::factory()->create(); 25 | 26 | expect($link->user)->toBeInstanceOf(User::class); 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.9", 10 | "@tailwindcss/typography": "^0.5.15", 11 | "alpinejs-notify": "^1.0.4", 12 | "autosize": "^6.0.1", 13 | "autoprefixer": "^10.4.20", 14 | "axios": "^1.7.7", 15 | "highlight.js": "^11.10.0", 16 | "laravel-vite-plugin": "^1.0", 17 | "postcss": "^8.4.47", 18 | "sortablejs": "^1.15.3", 19 | "tailwindcss": "^3.4.14", 20 | "vite": "^5.4.10" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Livewire/Concerns/NeedsVerifiedEmail.php: -------------------------------------------------------------------------------- 1 | user()?->hasVerifiedEmail()) { 15 | return false; 16 | } 17 | 18 | session()->flash('flash-message', 'You must verify your email address before you can continue.'); 19 | 20 | $this->redirectRoute('verification.notice', navigate: true); 21 | 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/View/Components/Footer.php: -------------------------------------------------------------------------------- 1 | app(Git::class)->getLatestTag()); 20 | 21 | return view('components.footer', [ 22 | 'version' => $version, 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2024_07_25_232940_optimize_database_settings.php: -------------------------------------------------------------------------------- 1 | {this.resize()}):this.setUpResizeObserver()},setInitialHeight:function(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/users.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/image-lightbox.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | image 5 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /tests/Unit/Models/LikeTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 11 | 12 | expect(array_keys($question->toArray()))->toBe([ 13 | 'id', 14 | 'user_id', 15 | 'question_id', 16 | 'created_at', 17 | 'updated_at', 18 | ]); 19 | }); 20 | 21 | test('relations', function () { 22 | $like = Like::factory()->create(); 23 | 24 | expect($like->user)->toBeInstanceOf(User::class) 25 | ->and($like->question)->toBeInstanceOf(Question::class); 26 | }); 27 | -------------------------------------------------------------------------------- /resources/views/livewire/bookmarks/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @forelse ($bookmarks as $bookmark) 3 | 8 | @empty 9 |
10 |

No bookmarks.

11 |
12 | @endforelse 13 | 14 | 19 |
20 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserTimezoneController.php: -------------------------------------------------------------------------------- 1 | $validated */ 18 | $validated = $request->validate([ 19 | 'timezone' => ['required', 'string', 'max:255', new ValidTimezone], 20 | ]); 21 | 22 | $request->session()->put('timezone', $validated['timezone']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/Concerns/RefreshOnCreate.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @template TModel of Model 14 | */ 15 | trait RefreshOnCreate 16 | { 17 | /** 18 | * {@inheritDoc} 19 | */ 20 | public function create($attributes = [], ?Model $parent = null) 21 | { 22 | $models = parent::create($attributes, $parent); 23 | 24 | return $models instanceof Model ? $models->refresh() : $models->map->refresh(); // @phpstan-ignore-line 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /tests/Unit/Models/BookmarkTest.php: -------------------------------------------------------------------------------- 1 | create()->fresh(); 11 | 12 | expect(array_keys($question->toArray()))->toBe([ 13 | 'id', 14 | 'user_id', 15 | 'question_id', 16 | 'created_at', 17 | 'updated_at', 18 | ]); 19 | }); 20 | 21 | test('relations', function () { 22 | $bookmark = Bookmark::factory()->create(); 23 | 24 | expect($bookmark->user)->toBeInstanceOf(User::class) 25 | ->and($bookmark->question)->toBeInstanceOf(Question::class); 26 | }); 27 | -------------------------------------------------------------------------------- /database/migrations/2024_07_25_183750_set_journal_mode_and_synchronous_settings.php: -------------------------------------------------------------------------------- 1 | false, 4 | 'isFollowing' => false, 5 | ]) 6 | 7 | @if(auth()->id() !== $id) 8 |
11 | 18 | 19 | 20 |
21 | @endif 22 | -------------------------------------------------------------------------------- /resources/views/components/icons/trash.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Arch/LivewireTest.php: -------------------------------------------------------------------------------- 1 | expect('App\Livewire') 7 | ->toBeClasses() 8 | ->ignoring('App\Livewire\Concerns') 9 | ->toExtend('Livewire\Component') 10 | ->ignoring('App\Livewire\Concerns') 11 | ->toHaveMethod('render') 12 | ->ignoring('App\Livewire\Concerns') 13 | ->toOnlyBeUsedIn([ 14 | 'App\Http\Controllers', 15 | 'App\Http\Livewire', 16 | 'App\Providers\AppServiceProvider', 17 | ]) 18 | ->ignoring('App\Livewire\Concerns') 19 | ->not->toUse(['redirect', 'to_route', 'back']); 20 | 21 | arch('livewire concerns') 22 | ->expect('App\Livewire\Concerns') 23 | ->toBeTraits(); 24 | -------------------------------------------------------------------------------- /resources/views/components/select-input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['options' => [], 'disabled' => false]) 2 | 3 | 16 | -------------------------------------------------------------------------------- /tests/Unit/Policies/UserPolicyTest.php: -------------------------------------------------------------------------------- 1 | create(); 9 | $target = User::factory()->create(); 10 | 11 | expect($user->can('follow', $target))->toBeTrue(); 12 | 13 | $user = User::factory()->create(); 14 | 15 | expect($user->can('follow', $user))->toBeFalse(); 16 | }); 17 | 18 | test('unfollow', function () { 19 | $user = User::factory()->create(); 20 | $target = User::factory()->create(); 21 | 22 | expect($user->can('unfollow', $target))->toBeTrue(); 23 | 24 | $user = User::factory()->create(); 25 | 26 | expect($user->can('unfollow', $user))->toBeFalse(); 27 | }); 28 | -------------------------------------------------------------------------------- /resources/views/components/icons/eye-off.blade.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /database/migrations/2024_07_25_212051_optimize_database_settings.php: -------------------------------------------------------------------------------- 1 | merge(['class' => $classes]) }} 14 | wire:navigate 15 | > 16 | {{ $slot }} 17 | 18 | -------------------------------------------------------------------------------- /database/factories/HashtagFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class HashtagFactory extends Factory 15 | { 16 | /** 17 | * @use RefreshOnCreate 18 | */ 19 | use RefreshOnCreate; 20 | 21 | /** 22 | * Define the model's default state. 23 | * 24 | * @return array 25 | */ 26 | public function definition(): array 27 | { 28 | return [ 29 | 'name' => $this->faker->unique()->word(), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | guard('web')->logout(); 26 | session()->invalidate(); 27 | session()->regenerateToken(); 28 | 29 | return back(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2024_02_17_090102_create_links_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('description'); 20 | $table->string('url'); 21 | $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/Livewire/Home/TrendingQuestions.php: -------------------------------------------------------------------------------- 1 | builder()->simplePaginate($this->perPage); 23 | 24 | return view('livewire.home.trending-questions', [ 25 | 'trendingQuestions' => $questions, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Rules/MaxUploads.php: -------------------------------------------------------------------------------- 1 | $this->maxUploads) { 26 | $fail("You can only upload $this->maxUploads images."); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Rules/VerifiedOnly.php: -------------------------------------------------------------------------------- 1 | user()?->is_verified || auth()->user()?->is_company_verified) { 20 | return; 21 | } 22 | 23 | $fail('This action is only available to verified users. Get verified in your profile settings.'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2024_05_11_141256_add_answer_updated_at_to_questions_table.php: -------------------------------------------------------------------------------- 1 | timestamp('answer_updated_at')->nullable()->after('answered_at'); 19 | }); 20 | 21 | DB::statement('UPDATE questions SET answer_updated_at = updated_at WHERE answered_at < updated_at'); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /resources/views/components/links/list-item.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'user' => null, 3 | 'link' => null, 4 | ]) 5 | 6 | @php 7 | $isUserProfileOwner = auth()->user()?->is($user); 8 | $linkWithRef = $link->url . (str_contains($link->url, '?') ? '&' : '?') . 'ref=pinkary'; 9 | @endphp 10 | 11 | 18 |
19 |

20 | {{ $link->description }} 21 |

22 |
23 |
24 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPaths([ 10 | __DIR__.'/app', 11 | __DIR__.'/bootstrap/app.php', 12 | __DIR__.'/config', 13 | __DIR__.'/database', 14 | __DIR__.'/public', 15 | ]) 16 | ->withSkip([ 17 | AddOverrideAttributeToOverriddenMethodsRector::class, 18 | ]) 19 | ->withPreparedSets( 20 | deadCode: true, 21 | codeQuality: true, 22 | typeDeclarations: true, 23 | privatization: true, 24 | earlyReturn: true, 25 | strictBooleans: true, 26 | ) 27 | ->withPhpSets(); 28 | -------------------------------------------------------------------------------- /database/migrations/2024_02_28_000521_create_notifications_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 18 | $table->string('type'); 19 | $table->morphs('notifiable'); 20 | $table->text('data'); 21 | $table->timestamp('read_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/Http/Citadel/Questions/IndexTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create([ 11 | 'email' => 'enunomaduro@gmail.com', 12 | ])); 13 | $this->get(QuestionResource::getUrl('index'))->assertSuccessful(); 14 | }); 15 | 16 | it('has a stats widget', function () { 17 | $this->actingAs(User::factory()->create([ 18 | 'email' => 'enunomaduro@gmail.com', 19 | ])); 20 | 21 | $response = $this->get(QuestionResource::getUrl('index')); 22 | 23 | $response->assertSeeLivewire(QuestionOverview::class); 24 | }); 25 | -------------------------------------------------------------------------------- /database/migrations/2024_05_01_170027_add_user_id_index_to_links_table.php: -------------------------------------------------------------------------------- 1 | index('user_id'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | */ 24 | public function down(): void 25 | { 26 | Schema::table('links', function (Blueprint $table): void { 27 | // 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2024_03_29_040310_create_followers_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->foreignIdFor(User::class, 'user_id')->constrained('users'); 20 | $table->foreignIdFor(User::class, 'follower_id')->constrained('users'); 21 | $table->unique(['user_id', 'follower_id']); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /app/Filament/Resources/QuestionResource/Pages/Index.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected function getHeaderWidgets(): array 23 | { 24 | return [ 25 | QuestionResource\Widgets\QuestionOverview::class, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/Citadel/Users/IndexTest.php: -------------------------------------------------------------------------------- 1 | count(10)->create(); 11 | 12 | Livewire::test(UserResource\Pages\Index::class) 13 | ->assertCanSeeTableRecords($users); 14 | }); 15 | 16 | it('can delete user', function () { 17 | $user = User::factory()->create(); 18 | $anotherUser = User::factory()->create(); 19 | 20 | Livewire::test(UserResource\Pages\Index::class) 21 | ->callTableAction('delete', $user); 22 | 23 | $this->assertDatabaseMissing('users', ['id' => $user->id]); 24 | $this->assertDatabaseHas('users', ['id' => $anotherUser->id]); 25 | }); 26 | -------------------------------------------------------------------------------- /resources/views/components/icons/camera.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /resources/views/components/textarea.blade.php: -------------------------------------------------------------------------------- 1 | @props(['autocomplete' => false, 'id' => null]) 2 | @php 3 | $componentId = $id ?? Str::uuid(); 4 | @endphp 5 | 14 | 15 | @if ($autocomplete === true) 16 | 17 | @endif 18 | -------------------------------------------------------------------------------- /app/Providers/PulseServiceProvider.php: -------------------------------------------------------------------------------- 1 | $user->email === 'enunomaduro@gmail.com' || $user->email === 'mrpunyapal@gmail.com'); 20 | 21 | Pulse::user(fn (User $user): array => [ 22 | 'name' => $user->name, 23 | 'extra' => $user->email, 24 | 'avatar' => $user->avatar_url, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Livewire/Home/QuestionsFollowing.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 23 | 24 | $questions = (new QuestionsFollowingFeed($user))->builder()->simplePaginate($this->perPage); 25 | 26 | return view('livewire.home.questions-following', [ 27 | 'followingQuestions' => $questions, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/views/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'active', 3 | ]) 4 | 5 | @php 6 | $classes = 7 | $active ?? false 8 | ? 'block w-full border-l-4 border-indigo-400 bg-indigo-50 py-2 pe-4 ps-3 text-start text-base font-medium text-indigo-700 transition duration-150 ease-in-out focus:border-indigo-700 focus:bg-indigo-100 focus:text-indigo-800 focus:outline-none' 9 | : 'block w-full border-l-4 border-transparent py-2 pe-4 ps-3 text-start text-base font-medium text-gray-600 transition duration-150 ease-in-out hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800 focus:border-gray-300 focus:bg-gray-50 focus:text-gray-800 focus:outline-none'; 10 | @endphp 11 | 12 | merge(['class' => $classes]) }} 14 | wire:navigate 15 | > 16 | {{ $slot }} 17 | 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 20 | 21 | return $user->hasVerifiedEmail() 22 | ? redirect()->intended(route('profile.show', [ 23 | 'username' => $user->username, 24 | ], absolute: false)) : view('auth.verify-email'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/migrations/2024_05_01_152116_add_from_id_and_to_id_indexes_to_questions_table.php: -------------------------------------------------------------------------------- 1 | index('from_id'); 18 | $table->index('to_id'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('questions', function (Blueprint $table): void { 28 | // 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /resources/js/click-handler.js: -------------------------------------------------------------------------------- 1 | const clickHandler = () => ({ 2 | 3 | handleNavigation(event) 4 | { 5 | if(window.getSelection().toString().length !== 0) { 6 | return; 7 | } 8 | 9 | const hasDataNavigateIgnore = (el) => { 10 | if (!el || el.dataset.parent === 'true') { 11 | return false; 12 | } 13 | if (el.dataset.navigateIgnore === 'true') { 14 | return true; 15 | } 16 | return hasDataNavigateIgnore(el.parentElement); 17 | }; 18 | 19 | if (! hasDataNavigateIgnore(event.target)) { 20 | const parentLink = this.$refs.parentLink 21 | if (parentLink) { 22 | parentLink.click(); 23 | } 24 | 25 | } 26 | 27 | } 28 | }) 29 | 30 | export { clickHandler } 31 | -------------------------------------------------------------------------------- /database/migrations/2024_02_25_144346_create_likes_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); 21 | $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); 22 | 23 | $table->unique(['user_id', 'question_id']); 24 | 25 | $table->timestamps(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/tags-input.js: -------------------------------------------------------------------------------- 1 | function i({state:a,splitKeys:n}){return{newTag:"",state:a,createTag:function(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag:function(t){this.state=this.state.filter(e=>e!==t)},reorderTags:function(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{["x-on:blur"]:"createTag()",["x-model"]:"newTag",["x-on:keydown"](t){["Enter",...n].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},["x-on:paste"](){this.$nextTick(()=>{if(n.length===0){this.createTag();return}let t=n.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{i as default}; 2 | -------------------------------------------------------------------------------- /database/migrations/2024_07_06_191351_create_bookmarks_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); 21 | $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); 22 | 23 | $table->unique(['user_id', 'question_id']); 24 | 25 | $table->timestamps(); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tests/Http/Profile/Timezone/UpdateTest.php: -------------------------------------------------------------------------------- 1 | post(route('profile.timezone.update'), [ 9 | 'timezone' => 'Europe/Madrid', 10 | ]); 11 | 12 | $response->assertStatus(200); 13 | }); 14 | 15 | test('logged user can update timezone', function () { 16 | $user = User::factory()->create(); 17 | 18 | $response = $this->actingAs($user)->post(route('profile.timezone.update'), [ 19 | 'timezone' => 'Europe/Madrid', 20 | ]); 21 | 22 | $response->assertStatus(200); 23 | }); 24 | 25 | test('timezone must be valid', function () { 26 | $response = $this->post(route('profile.timezone.update'), [ 27 | 'timezone' => 'Nuno/Maduro', 28 | ]); 29 | 30 | $response->assertStatus(302); 31 | }); 32 | -------------------------------------------------------------------------------- /database/factories/PanAnalyticFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class PanAnalyticFactory extends Factory 13 | { 14 | /** 15 | * Define the model's default state. 16 | * 17 | * @return array 18 | */ 19 | public function definition(): array 20 | { 21 | return [ 22 | 'name' => $this->faker->name, 23 | 'impressions' => fn (array $attributes): int => type($attributes['hovers'])->asInt() + type($attributes['clicks'])->asInt(), 24 | 'hovers' => $this->faker->randomNumber(), 25 | 'clicks' => $this->faker->randomNumber(), 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 18 | $table->mediumText('value'); 19 | $table->integer('expiration'); 20 | }); 21 | 22 | Schema::create('cache_locks', function (Blueprint $table): void { 23 | $table->string('key')->primary(); 24 | $table->string('owner'); 25 | $table->integer('expiration'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | dailyAt('13:00'); 13 | Schedule::command(SendUnreadNotificationEmailsCommand::class, ['--weekly' => true])->weekly()->mondays()->at('13:00'); 14 | Schedule::command(PerformDatabaseBackupCommand::class)->everySixHours(); 15 | Schedule::command(DeleteNonEmailVerifiedUsersCommand::class)->hourly(); 16 | Schedule::command(SyncVerifiedUsersCommand::class)->daily(); 17 | Schedule::job(CleanUnusedUploadedImages::class)->hourly(); 18 | -------------------------------------------------------------------------------- /tests/Http/Home/FollowingTest.php: -------------------------------------------------------------------------------- 1 | get('/for-you'); 10 | 11 | $response->assertRedirect('/following'); 12 | }); 13 | 14 | it('can see the "following" view', function () { 15 | $user = User::factory()->create(); 16 | 17 | $response = $this->actingAs($user)->get(route('home.following')); 18 | 19 | $response->assertOk() 20 | ->assertSee('Following') 21 | ->assertSeeLivewire(QuestionsFollowing::class); 22 | }); 23 | 24 | it('guest can see the "following" view', function () { 25 | $response = $this->get(route('home.following')); 26 | 27 | $response->assertOk() 28 | ->assertSee('Log in or sign up to access personalized content'); 29 | }); 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserIsVerifiedController.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 19 | 20 | SyncVerifiedUser::dispatchSync($user); 21 | 22 | $user = type($user->fresh())->as(User::class); 23 | 24 | $user->is_verified 25 | ? session()->flash('flash-message', 'Your account has been verified.') 26 | : session()->flash('flash-message', 'Your account is not verified yet.'); 27 | 28 | return to_route('profile.edit'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /resources/views/components/icons/globe.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/Http/Middleware/EnsureVerifiedEmailsForSignInUsers.php: -------------------------------------------------------------------------------- 1 | check()) { 22 | return $next($request); 23 | } 24 | 25 | $user = type($request->user())->as(User::class); 26 | 27 | if ($user->hasVerifiedEmail()) { 28 | return $next($request); 29 | } 30 | 31 | return to_route('verification.notice'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Http/Hashtag/ShowTest.php: -------------------------------------------------------------------------------- 1 | create(); 9 | App\Models\Question::factory()->create(['answer' => '#hashtag1']); 10 | App\Models\Question::factory()->create(['answer' => 'not a hashtag in sight']); 11 | 12 | $response = $this->actingAs($user)->get('/hashtag/hashtag1'); 13 | 14 | $response 15 | ->assertOk() 16 | ->assertSee('#hashtag1') 17 | ->assertDontSee('not a hashtag in sight'); 18 | }); 19 | 20 | it('guests are allowed to view', function () { 21 | App\Models\Question::factory()->create(['answer' => '#hashtag1']); 22 | 23 | $response = $this->get('/hashtag/hashtag1'); 24 | 25 | $response 26 | ->assertOk() 27 | ->assertSee('#hashtag1'); 28 | }); 29 | -------------------------------------------------------------------------------- /app/Services/ParsableContentProviders/MentionProviderParsable.php: -------------------------------------------------------------------------------- 1 | ]*>.*?<\/\2>)|@([a-z0-9_]+)/is', 18 | fn (array $matches): string => $matches[1] !== '' 19 | ? $matches[1] 20 | : '@'.$matches[3].'', 21 | $content 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/views/livewire/about-users-avatars.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | @foreach ($users as $user) 6 | 10 | {{ $user->username }} 15 | 16 | @endforeach 17 |
18 | -------------------------------------------------------------------------------- /resources/views/livewire/flash-messages/show.blade.php: -------------------------------------------------------------------------------- 1 |
15 |
19 | 20 | 28 |
29 | -------------------------------------------------------------------------------- /tests/Unit/Rules/ValidTimezoneTest.php: -------------------------------------------------------------------------------- 1 | $this->fail($errorMessage); 11 | 12 | $rule->validate('timezone', $timezone, $fail); 13 | 14 | expect(true)->toBeTrue(); 15 | })->with([ 16 | 'America/New_York', 17 | 'UTC', 18 | 'Europe/London', 19 | 'Asia/Tokyo', 20 | ]); 21 | 22 | test('invalid timezone', function (string $timezone) { 23 | $rule = new ValidTimezone(); 24 | 25 | $fail = fn (string $errorMessage) => $this->fail($errorMessage); 26 | 27 | $rule->validate('timezone', $timezone, $fail); 28 | 29 | expect(true)->toBeFalse(); 30 | })->with([ 31 | 'America/New_Yorkk', 32 | 'UTCC', 33 | 'Europe/Londonn', 34 | 'Asia/Tokyo0', 35 | ])->fails(); 36 | -------------------------------------------------------------------------------- /resources/js/show-more.js: -------------------------------------------------------------------------------- 1 | const showMore = () => ({ 2 | 3 | maxHeight: 600, 4 | 5 | initialHeight: 0, 6 | 7 | open: false, 8 | 9 | showMore: false, 10 | 11 | init() { 12 | this.initialHeight = this.$refs.parentDiv.scrollHeight; 13 | 14 | if (this.initialHeight > this.maxHeight + 40) { 15 | this.$refs.parentDiv.style.maxHeight = this.maxHeight + 'px'; 16 | this.showMore = true; 17 | } 18 | }, 19 | 20 | showButtonAction() { 21 | let height = this.open === false ? this.initialHeight : this.maxHeight; 22 | 23 | this.$refs.parentDiv.style.maxHeight = height + 'px'; 24 | 25 | this.open = ! this.open; 26 | }, 27 | 28 | showMoreButtonText() { 29 | if (this.open === false) { 30 | return 'Show more'; 31 | } 32 | 33 | return 'Show less'; 34 | }, 35 | }) 36 | 37 | export { showMore } 38 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link-profile.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $trigger }} 4 |
5 | 6 | 22 |
23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | laravel.test: 3 | build: 4 | context: ./vendor/laravel/sail/runtimes/8.3 5 | dockerfile: Dockerfile 6 | args: 7 | WWWGROUP: '${WWWGROUP}' 8 | image: sail-8.3/app 9 | extra_hosts: 10 | - 'host.docker.internal:host-gateway' 11 | ports: 12 | - '${APP_PORT:-80}:80' 13 | - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' 14 | environment: 15 | WWWUSER: '${WWWUSER}' 16 | LARAVEL_SAIL: 1 17 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' 18 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' 19 | IGNITION_LOCAL_SITES_PATH: '${PWD}' 20 | volumes: 21 | - '.:/var/www/html' 22 | networks: 23 | - sail 24 | depends_on: { } 25 | networks: 26 | sail: 27 | driver: bridge 28 | -------------------------------------------------------------------------------- /app/Livewire/AboutUsersAvatars.php: -------------------------------------------------------------------------------- 1 | with('links') 21 | ->withCount(['questionsReceived as answered_questions_count' => function (Builder $query): void { 22 | $query->whereNotNull('answer'); 23 | }]) 24 | ->orderBy('answered_questions_count', 'desc') 25 | ->limit(14) 26 | ->get() 27 | ->shuffle(); 28 | 29 | return view('livewire.about-users-avatars', [ 30 | 'users' => $users, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/views/components/post-divider.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'link' => null, 3 | 'text' => null, 4 | ]) 5 | 6 | @if ($link !== null && $text !== null) 7 |
8 | 9 | 10 | 11 | 12 | {{ $text }} 13 | 14 |
15 | @else 16 |
17 | 18 |
19 | @endif 20 | 21 | -------------------------------------------------------------------------------- /app/Livewire/Views/Create.php: -------------------------------------------------------------------------------- 1 | $postIds 18 | */ 19 | #[Renderless] 20 | public function store(array $postIds): void 21 | { 22 | $questions = collect($postIds)->map(fn (string $postId): Question => (new Question())->setRawAttributes(['id' => $postId])); 23 | 24 | IncrementViews::dispatchUsingSession($questions); 25 | } 26 | 27 | /** 28 | * Render the component. 29 | */ 30 | public function render(): string 31 | { 32 | return <<<'HTML' 33 |
34 | HTML; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2024_10_13_234344_create_pan_tables.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | 20 | $table->unsignedBigInteger('impressions')->default(0); 21 | $table->unsignedBigInteger('hovers')->default(0); 22 | $table->unsignedBigInteger('clicks')->default(0); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('pan_analytics'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 19 | 20 | if ($user->hasVerifiedEmail()) { 21 | return redirect()->intended(route('profile.show', [ 22 | 'username' => $user->username, 23 | ], absolute: false)); 24 | } 25 | 26 | $user->sendEmailVerificationNotification(); 27 | 28 | session()->flash('flash-message', 'Verification email sent.'); 29 | 30 | return back(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Livewire/Views/CreateTest.php: -------------------------------------------------------------------------------- 1 | assertStatus(200); 13 | }); 14 | 15 | test('updateViews dispatches the job with the correct data', function () { 16 | Queue::fake(); 17 | 18 | $component = Livewire::test(Create::class); 19 | 20 | $postIds = [1, 2, 3]; 21 | 22 | $questions = collect($postIds)->map(fn (string $postId): Question => (new Question())->setRawAttributes(['id' => $postId])); 23 | 24 | $component->call('store', $postIds); 25 | 26 | Queue::assertPushed(function (IncrementViews $job) use ($questions) { 27 | return invade($job)->viewables->pluck('id')->toArray() === $questions->pluck('id')->toArray(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /app/Jobs/SyncVerifiedUser.php: -------------------------------------------------------------------------------- 1 | user->fresh())->as(User::class); 30 | 31 | $user->update([ 32 | 'is_verified' => $user->github_username && $github->isSponsor($user->github_username), 33 | 'is_company_verified' => $user->github_username && $github->isCompanySponsor($user->github_username), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Services/BioTest.php: -------------------------------------------------------------------------------- 1 | full stack dev and here is my link example.com.'; 7 | 8 | $provider = new App\Services\ParsableBio(); 9 | 10 | expect($provider->parse($content))->toBe('hi I'm a <b>full stack dev</b> and here is my link <a href="https://example.com">example.com</a>.'); 11 | 12 | $content = 'hi I\'m a full stack dev and here is my link https://example.com'; 13 | 14 | expect($provider->parse($content))->toBe('hi I'm a <a onload="alert('XSS')">full stack dev</a> and here is my link https://example.com'); 15 | }); 16 | 17 | test('empty', function () { 18 | $content = ''; 19 | 20 | $provider = (new App\Services\ParsableBio); 21 | 22 | expect($provider->parse($content))->toBe(''); 23 | }); 24 | -------------------------------------------------------------------------------- /resources/views/livewire/questions/index.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if ($pinnedQuestion) 3 | 9 | @endif 10 | 11 | @foreach ($questions as $question) 12 | 19 | @endforeach 20 | 21 | 26 |
27 | -------------------------------------------------------------------------------- /app/Livewire/Navigation/NotificationsCount/Show.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 33 | 34 | return view('livewire.navigation.notifications-count.show', [ 35 | 'count' => $user->notifications()->count(), 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/views/support.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 | 9 | Back 10 | 11 | 12 |
13 |
14 |

Support

15 |

Last Updated: 02 March 2024

16 | 17 |

18 | If you have any questions or need help, please feel free to contact us at team@pinkary.com . 19 |

20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /tests/Http/Citadel/Users/IndexTest.php: -------------------------------------------------------------------------------- 1 | get(UserResource::getUrl('index', isAbsolute: false)); 10 | 11 | $response->assertStatus(302)->assertRedirect(route('login')); 12 | }); 13 | 14 | it('is only accessible to nuno', function () { 15 | $user = User::factory()->create([ 16 | 'email' => 'enunomaduro@gmail.com', 17 | ]); 18 | 19 | $response = $this->actingAs($user)->get(UserResource::getUrl('index', isAbsolute: false)); 20 | 21 | $response->assertStatus(200)->assertSee('Users'); 22 | }); 23 | 24 | it('is not accessible to other users', function () { 25 | $user = User::factory()->create([ 26 | 'email' => 'nuno@laravel.com', 27 | ]); 28 | 29 | $response = $this->actingAs($user)->get(UserResource::getUrl('index', isAbsolute: false)); 30 | 31 | $response->assertStatus(403); 32 | }); 33 | -------------------------------------------------------------------------------- /forge-deployment.sh: -------------------------------------------------------------------------------- 1 | cd /home/forge/pinkary.com 2 | 3 | $FORGE_PHP artisan down 4 | 5 | git pull origin $FORGE_SITE_BRANCH 6 | 7 | git fetch 8 | 9 | $FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader 10 | 11 | $FORGE_PHP artisan cache:clear 12 | 13 | $FORGE_PHP artisan config:clear 14 | $FORGE_PHP artisan config:cache 15 | 16 | $FORGE_PHP artisan view:clear 17 | $FORGE_PHP artisan view:cache 18 | 19 | $FORGE_PHP artisan event:clear 20 | $FORGE_PHP artisan event:cache 21 | 22 | $FORGE_PHP artisan route:clear 23 | $FORGE_PHP artisan route:cache 24 | 25 | $FORGE_PHP artisan queue:restart 26 | 27 | ( flock -w 10 9 || exit 1 28 | echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock 29 | 30 | export NODE_OPTIONS=--max-old-space-size=32768 31 | 32 | npm install 33 | npm run build 34 | 35 | if [ -f artisan ]; then 36 | $FORGE_PHP artisan migrate --force 37 | fi 38 | 39 | $FORGE_PHP artisan pulse:restart 40 | 41 | $FORGE_PHP artisan up 42 | -------------------------------------------------------------------------------- /app/Console/Commands/SyncVerifiedUsersCommand.php: -------------------------------------------------------------------------------- 1 | orWhere('is_company_verified', true) 34 | ->get() 35 | ->each(fn (User $user) => SyncVerifiedUser::dispatch($user)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/views/components/icons/github.blade.php: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /database/factories/LikeFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class LikeFactory extends Factory 17 | { 18 | /** 19 | * @use RefreshOnCreate 20 | */ 21 | use RefreshOnCreate; 22 | 23 | /** 24 | * The name of the factory's corresponding model. 25 | * 26 | * @var class-string 27 | */ 28 | protected $model = Like::class; 29 | 30 | /** 31 | * Define the model's default state. 32 | * 33 | * @return array 34 | */ 35 | public function definition(): array 36 | { 37 | return [ 38 | 'user_id' => User::factory(), 39 | 'question_id' => Question::factory(), 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/sponsors.php: -------------------------------------------------------------------------------- 1 | collect(explode(',', type(env('SPONSORS_GITHUB_USERNAMES', ''))->asString()))->map( 19 | fn (string $username): string => trim($username) 20 | )->filter()->values()->all(), 21 | 22 | 'github_company_usernames' => collect(explode(',', type(env('SPONSORS_GITHUB_COMPANY_USERNAMES', ''))->asString()))->map( 23 | fn (string $username): string => trim($username) 24 | )->filter()->values()->all(), 25 | ]; 26 | -------------------------------------------------------------------------------- /app/Notifications/QuestionAnswered.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function via(object $notifiable): array 26 | { 27 | return ['database']; 28 | } 29 | 30 | /** 31 | * Get the array representation of the notification. 32 | * 33 | * @return array 34 | */ 35 | public function toDatabase(object $notifiable): array 36 | { 37 | return [ 38 | 'question_id' => $this->question->id, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /database/factories/LinkFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class LinkFactory extends Factory 16 | { 17 | /** 18 | * @use RefreshOnCreate 19 | */ 20 | use RefreshOnCreate; 21 | 22 | /** 23 | * The name of the factory's corresponding model. 24 | * 25 | * @var class-string 26 | */ 27 | protected $model = Link::class; 28 | 29 | /** 30 | * Define the model's default state. 31 | * 32 | * @return array 33 | */ 34 | public function definition(): array 35 | { 36 | return [ 37 | 'description' => $this->faker->sentence, 38 | 'url' => $this->faker->url, 39 | 'user_id' => User::factory(), 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2024_02_20_213416_create_questions_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->foreignIdFor(User::class, 'from_id')->constrained('users')->cascadeOnDelete(); 20 | $table->foreignIdFor(User::class, 'to_id')->constrained('users')->cascadeOnDelete(); 21 | 22 | $table->text('content'); 23 | $table->boolean('anonymously')->default(false); 24 | $table->text('answer')->nullable(); 25 | 26 | $table->timestamp('answered_at')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /public/js/filament/forms/components/key-value.js: -------------------------------------------------------------------------------- 1 | function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; 2 | -------------------------------------------------------------------------------- /resources/views/components/icons/qr-code.blade.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/Unit/Citadel/Questions/QuestionOverviewTest.php: -------------------------------------------------------------------------------- 1 | count(51)->create(); 12 | 13 | Question::factory()->count(10)->create([ 14 | 'is_reported' => true, 15 | ]); 16 | 17 | Question::factory()->count(5)->create([ 18 | 'is_ignored' => true, 19 | ]); 20 | 21 | Question::factory()->count(5)->create([ 22 | 'is_reported' => true, 23 | 'is_ignored' => true, 24 | ]); 25 | 26 | $component = Livewire::test(QuestionOverview::class); 27 | 28 | $component->assertSee('Total Questions'); 29 | $component->assertSee('71'); 30 | 31 | $component->assertSee('Reported Questions'); 32 | $component->assertSee('15'); 33 | 34 | $component->assertSee('Ignored Questions'); 35 | $component->assertSee('10'); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/Unit/Mail/PendingNotificationsTest.php: -------------------------------------------------------------------------------- 1 | create(); 10 | 11 | $mail = new PendingNotifications($user, 1); 12 | 13 | $envelope = $mail->envelope(); 14 | 15 | expect($envelope->subject) 16 | ->toBe('🌸 Pinkary: You Have 1 Notification! - '.now()->format('F j, Y')); 17 | }); 18 | 19 | test('content', function () { 20 | $user = User::factory()->create(); 21 | 22 | $mail = new PendingNotifications($user, 1); 23 | 24 | foreach ([ 25 | '# Hello, '.$user->name.'!', 26 | "We've noticed you have 1 notification. You can view notifications by clicking the button below.", 27 | 'If you no longer wish to receive these emails, you can change your "Mail Preference Time" in your [profile settings]('.config('app.url').'/profile).', 28 | ] as $line) { 29 | $mail->assertSeeInText($line); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /database/factories/BookmarkFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class BookmarkFactory extends Factory 17 | { 18 | /** 19 | * @use RefreshOnCreate 20 | */ 21 | use RefreshOnCreate; 22 | 23 | /** 24 | * The name of the factory's corresponding model. 25 | * 26 | * @var class-string 27 | */ 28 | protected $model = Bookmark::class; 29 | 30 | /** 31 | * Define the model's default state. 32 | * 33 | * @return array 34 | */ 35 | public function definition(): array 36 | { 37 | return [ 38 | 'user_id' => User::factory(), 39 | 'question_id' => Question::factory(), 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Livewire/Bookmarks/Index.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 30 | 31 | return view('livewire.bookmarks.index', [ 32 | 'user' => $user, 33 | 'bookmarks' => $user->bookmarks() 34 | ->with('question') 35 | ->orderBy('created_at', 'desc') 36 | ->simplePaginate($this->perPage), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/share-profile.js: -------------------------------------------------------------------------------- 1 | const shareProfile = () => ({ 2 | 3 | isVisible: false, 4 | 5 | init() { 6 | if (navigator.share) { 7 | this.isVisible = true 8 | } 9 | }, 10 | 11 | share(options) { 12 | navigator.share(options) 13 | }, 14 | 15 | twitter(options) { 16 | let text = options.question ? options.question + '%0A%0A' : '' 17 | 18 | text = text 19 | .replace(' [👀 see the code on Pinkary 👀] ', "%0A%0A[👀 see the code on Pinkary 👀]%0A%0A") 20 | .replace(/<\/?[^>]+(>|$)/g, "") 21 | .replace(/`/g, '') 22 | .replace(/&/g, '&') 23 | .replace(/</g, '<') 24 | .replace(/>/g, '>') 25 | .replace(/"/g, '"') 26 | .replace(/'/g, "'"); 27 | 28 | window.open( 29 | `https://twitter.com/intent/tweet?text=${text}${options.message}:&url=${options.url}`, 30 | "_blank" 31 | ) 32 | } 33 | }) 34 | 35 | export { shareProfile } 36 | -------------------------------------------------------------------------------- /tests/Unit/Services/AvatarTest.php: -------------------------------------------------------------------------------- 1 | create(), 11 | ); 12 | 13 | expect($avatar->url())->toBe(asset('img/default-avatar.png')); 14 | }); 15 | 16 | test('avatar url with gravatar', function () { 17 | $user = User::factory()->create(['email' => 'enunomaduro@gmail.com']); 18 | $gravHash = hash('sha256', mb_strtolower($user->email)); 19 | 20 | $avatar = new Avatar( 21 | user: $user, 22 | ); 23 | 24 | expect($avatar->url())->toBe("https://gravatar.com/avatar/{$gravHash}?s=300&d=404"); 25 | }); 26 | 27 | test('avatar url with github', function () { 28 | $user = User::factory()->create(['github_username' => 'nunomaduro']); 29 | 30 | $avatar = new Avatar( 31 | user: $user, 32 | ); 33 | 34 | expect($avatar->url('github'))->toBe('https://avatars.githubusercontent.com/nunomaduro'); 35 | }); 36 | -------------------------------------------------------------------------------- /app/Http/Controllers/QuestionController.php: -------------------------------------------------------------------------------- 1 | to_id === $user->id, 404); 22 | 23 | $parentQuestions = []; 24 | $parentQuestion = $question->parent; 25 | 26 | do { 27 | $parentQuestions[] = $parentQuestion; 28 | } while ($parentQuestion = $parentQuestion?->parent); 29 | 30 | $parentQuestions = collect($parentQuestions)->filter()->reverse()->all(); 31 | 32 | return view('questions.show', [ 33 | 'question' => $question, 34 | 'parentQuestions' => $parentQuestions, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Notifications/UserMentioned.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function via(object $notifiable): array 30 | { 31 | return ['database']; 32 | } 33 | 34 | /** 35 | * Get the array representation of the notification. 36 | * 37 | * @return array 38 | */ 39 | public function toDatabase(object $notifiable): array 40 | { 41 | return [ 42 | 'question_id' => $this->question->id, 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /resources/views/livewire/home/trending-questions.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if ($trendingQuestions->isEmpty()) 3 |
4 |

There is no trending questions right now.

5 |
6 | @else 7 |
8 | @foreach ($trendingQuestions as $question) 9 | 16 | @endforeach 17 | 18 | 23 |
24 | @endif 25 |
26 | -------------------------------------------------------------------------------- /tests/Console/PerformDatabaseBackupCommandTest.php: -------------------------------------------------------------------------------- 1 | once() 11 | ->with(database_path('database.sqlite'), Mockery::type('string')); 12 | 13 | File::shouldReceive('glob') 14 | ->once() 15 | ->with(database_path('backups/*.sql')) 16 | ->andReturn([ 17 | database_path('backups/backup-1.sql'), 18 | database_path('backups/backup-2.sql'), 19 | database_path('backups/backup-3.sql'), 20 | database_path('backups/backup-4.sql'), 21 | database_path('backups/backup-5.sql'), 22 | ]); 23 | 24 | File::shouldReceive('delete') 25 | ->times(1) 26 | ->with(database_path('backups/backup-1.sql')) 27 | ->andReturnTrue(); 28 | 29 | $this->artisan(PerformDatabaseBackupCommand::class) 30 | ->assertExitCode(0); 31 | }); 32 | -------------------------------------------------------------------------------- /app/Http/Controllers/NotificationController.php: -------------------------------------------------------------------------------- 1 | data['question_id']))->as(Question::class); 28 | 29 | if ($question->answer !== null) { 30 | $notification->delete(); 31 | } 32 | 33 | return to_route('questions.show', [ 34 | 'username' => $question->to->username, 35 | 'question' => $question, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/views/layouts/about.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @include('layouts.components.head') 5 | 6 | 10 | 11 | 12 |
13 |
14 |
15 | @if (! request()->routeIs('about')) 16 | @include('layouts.navigation') 17 | @endif 18 |
19 | 20 |
21 | {{ $slot }} 22 |
23 |
24 | 25 | 26 |
27 | @livewireScriptConfig 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/Http/Requests/UserAvatarUpdateRequest.php: -------------------------------------------------------------------------------- 1 | > 18 | */ 19 | public function rules(): array 20 | { 21 | return [ 22 | 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png', 'max:2048'], 23 | ]; 24 | } 25 | 26 | /** 27 | * Get the error messages for the defined validation rules. 28 | * 29 | * @return array 30 | */ 31 | public function messages(): array 32 | { 33 | return [ 34 | 'avatar.max' => 'The avatar may not be greater than 2MB.', 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Notifications/QuestionCreated.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public function via(object $notifiable): array 29 | { 30 | return [ 31 | 'database', 32 | ]; 33 | } 34 | 35 | /** 36 | * Get the array representation of the notification. 37 | * 38 | * @return array 39 | */ 40 | public function toDatabase(object $notifiable): array 41 | { 42 | return [ 43 | 'question_id' => $this->question->id, 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Http/VerificationNotificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 13 | 'email_verified_at' => null, 14 | ]); 15 | 16 | $this->actingAs($user) 17 | ->post('email/verification-notification') 18 | ->assertRedirect('/'); 19 | 20 | Notification::assertSentTo($user, VerifyEmail::class); 21 | }); 22 | 23 | test('does not send verification notification if email is verified', function () { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create([ 27 | 'email_verified_at' => now(), 28 | ]); 29 | 30 | $this->actingAs($user) 31 | ->post('email/verification-notification') 32 | ->assertRedirect(route('profile.show', [ 33 | 'username' => $user->username, 34 | ])); 35 | 36 | Notification::assertNothingSent(); 37 | }); 38 | -------------------------------------------------------------------------------- /app/Services/ParsableBio.php: -------------------------------------------------------------------------------- 1 | > $providers 18 | */ 19 | public function __construct(private array $providers = [ 20 | StripProviderParsable::class, 21 | MentionProviderParsable::class, 22 | HashtagProviderParsable::class, 23 | ]) {} 24 | 25 | /** 26 | * Parses the given content. 27 | */ 28 | public function parse(string $content): string 29 | { 30 | if ($content === '') { 31 | return ''; 32 | } 33 | 34 | return (new ParsableContent($this->providers))->parse($content); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/UpdatePasswordController.php: -------------------------------------------------------------------------------- 1 | user())->as(User::class); 21 | 22 | /** @var array $validated */ 23 | $validated = $request->validateWithBag('updatePassword', [ 24 | 'current_password' => ['required', 'current_password'], 25 | 'password' => ['required', Password::defaults(), 'confirmed'], 26 | ]); 27 | 28 | $user->update([ 29 | 'password' => Hash::make($validated['password']), 30 | ]); 31 | 32 | session()->flash('flash-message', 'Password updated.'); 33 | 34 | return back(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resources/views/home/following.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | @auth 7 | 8 | @else 9 |
10 |
Log in or sign up to access personalized content.
11 | 12 | 16 | Log In 17 | 18 | 22 | Register 23 | 24 |
25 | @endauth 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /tests/Unit/Rules/RecaptchaTest.php: -------------------------------------------------------------------------------- 1 | Http::response([ 13 | 'success' => true, 14 | ]), 15 | ]); 16 | 17 | $fail = fn (string $errorMessage) => $this->fail($errorMessage); 18 | 19 | $rule->validate('g-recaptcha-response', 'valid', $fail); 20 | 21 | expect(true)->toBeTrue(); 22 | }); 23 | 24 | it('does not verify the recaptcha response', function () { 25 | $rule = new Recaptcha('127.0.0.1'); 26 | 27 | $response = Http::fake([ 28 | 'https://www.google.com/recaptcha/api/siteverify' => Http::response([ 29 | 'success' => false, 30 | ]), 31 | ]); 32 | 33 | $fail = fn (string $errorMessage) => $this->fail($errorMessage); 34 | 35 | $rule->validate('g-recaptcha-response', 'valid', $fail); 36 | 37 | expect(true)->toBeFalse(); 38 | })->fails(); 39 | -------------------------------------------------------------------------------- /database/migrations/2024_08_07_203106_create_hashtags_table.php: -------------------------------------------------------------------------------- 1 | id(); 20 | $table->string('name')->unique(); 21 | $table->timestamps(); 22 | 23 | $table->rawIndex('name collate nocase', 'name_collate_nocase'); 24 | }); 25 | 26 | Schema::create('hashtag_question', function (Blueprint $table): void { 27 | $table->id(); 28 | $table->foreignIdFor(Hashtag::class)->constrained()->cascadeOnDelete(); 29 | $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); 30 | 31 | $table->unique(['hashtag_id', 'question_id']); 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /resources/views/components/modal-qr-code.blade.php: -------------------------------------------------------------------------------- 1 | 6 |
16 | 21 | 22 |
23 | Close 24 | 30 | Download 31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'highlight.js/styles/tomorrow-night-blue.css'; 2 | 3 | [x-cloak] { 4 | display: none; 5 | } 6 | 7 | :not(.answer)>pre { 8 | padding-left: 1rem; 9 | padding-right: 1rem; 10 | } 11 | 12 | .hover\:darken-gradient:hover { 13 | filter: saturate(2.5); 14 | } 15 | 16 | /** See /resources/js/particles-effect.js */ 17 | particleEffect { 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | opacity: 0; 22 | pointer-events: none; 23 | border-radius: 50%; 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | height: 5px; 28 | width: 5px 29 | } 30 | 31 | ::-webkit-scrollbar-track { 32 | @apply bg-gray-900; 33 | } 34 | 35 | ::-webkit-scrollbar-thumb { 36 | @apply bg-gray-700; 37 | } 38 | 39 | ::-webkit-scrollbar-thumb:hover { 40 | background: #c9cbcd 41 | } 42 | 43 | .dark ::-webkit-scrollbar-track { 44 | @apply bg-gray-800; 45 | } 46 | 47 | .dark ::-webkit-scrollbar-thumb { 48 | @apply bg-gray-600; 49 | } 50 | 51 | .dark ::-webkit-scrollbar-thumb:hover { 52 | @apply bg-gray-600; 53 | } 54 | 55 | @tailwind base; 56 | @tailwind components; 57 | @tailwind utilities; 58 | -------------------------------------------------------------------------------- /database/migrations/2024_02_25_202236_add_uuid_to_questions_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 21 | $table->foreignIdFor(User::class, 'from_id')->constrained('users')->cascadeOnDelete(); 22 | $table->foreignIdFor(User::class, 'to_id')->constrained('users')->cascadeOnDelete(); 23 | 24 | $table->text('content'); 25 | $table->text('answer')->nullable(); 26 | $table->timestamp('answered_at')->nullable(); 27 | $table->boolean('anonymously')->default(false); 28 | $table->boolean('is_reported')->default(false); 29 | 30 | $table->timestamps(); 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tests/Unit/Rules/MaxUploadsTest.php: -------------------------------------------------------------------------------- 1 | maxUploads)->toBe(1); 12 | }); 13 | 14 | test('passes when the number of uploads is less than the limit', function () { 15 | Storage::fake('public'); 16 | $rule = new MaxUploads(1); 17 | 18 | $image = UploadedFile::fake()->image('image.jpg'); 19 | 20 | $rule->validate('image', [$image], function () { 21 | $this->fail('The validation callback should not be called.'); 22 | }); 23 | 24 | expect(true)->toBeTrue(); 25 | }); 26 | 27 | test('fails when the number of uploads is greater than the limit', function () { 28 | Storage::fake('public'); 29 | $rule = new MaxUploads(1); 30 | 31 | $image1 = UploadedFile::fake()->image('image1.jpg'); 32 | $image2 = UploadedFile::fake()->image('image2.jpg'); 33 | 34 | $rule->validate('image', [$image1, $image2], function (string $errorMessage) { 35 | expect($errorMessage)->toBe('You can only upload 1 images.'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /app/Rules/NoBlankCharacters.php: -------------------------------------------------------------------------------- 1 | asString(); 26 | 27 | if (preg_match(self::BLANK_CHARACTERS_PATTERN, $value) || preg_match(self::FORMAT_CHARACTERS_PATTERN, $value)) { 28 | $fail('The :attribute field cannot contain blank characters.'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Services/ParsableContentProviders/HashtagProviderParsable.php: -------------------------------------------------------------------------------- 1 | ]*>.*?<\/\2>)|(?#%s', 28 | "/hashtag/{$sanitizedHashtag}", 29 | $sanitizedHashtag 30 | ); 31 | }, 32 | $content 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 4 |
5 | 6 |
10 | @csrf 11 | 12 |
13 | 17 | 18 | 26 | 27 | 31 |
32 | 33 |
34 | 35 | {{ __('Confirm') }} 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /app/Services/Avatar.php: -------------------------------------------------------------------------------- 1 | user->github_username) { 26 | return "https://avatars.githubusercontent.com/{$this->user->github_username}"; 27 | } 28 | 29 | if ($service === 'gravatar') { 30 | $gravatarHash = hash('sha256', mb_strtolower($this->user->email)); 31 | $gravatarUrl = "https://gravatar.com/avatar/{$gravatarHash}?s=300&d=404"; 32 | $headers = get_headers($gravatarUrl); 33 | if ($headers !== false && ! in_array('HTTP/1.1 404 Not Found', $headers, true)) { 34 | return $gravatarUrl; 35 | } 36 | } 37 | 38 | return asset('img/default-avatar.png'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Http/ConfirmPasswordTest.php: -------------------------------------------------------------------------------- 1 | get('/confirm-password'); 9 | 10 | $response->assertRedirect('/login'); 11 | }); 12 | 13 | test('confirm password screen can be rendered', function () { 14 | $user = User::factory()->create(); 15 | 16 | $this->withoutExceptionHandling(); 17 | 18 | $response = $this->actingAs($user)->get('/confirm-password'); 19 | 20 | $response->assertOk(); 21 | }); 22 | 23 | test('password can be confirmed', function () { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response 31 | ->assertRedirect() 32 | ->assertSessionHasNoErrors(); 33 | }); 34 | 35 | test('password is not confirmed with invalid password', function () { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | }); 44 | --------------------------------------------------------------------------------