├── public
├── favicon.ico
├── assets
│ └── images
│ │ ├── 419.png
│ │ ├── logo.png
│ │ ├── favicon.ico
│ │ ├── og-image.png
│ │ ├── logo-icon.png
│ │ ├── github-avatar.jpg
│ │ └── sentry-wordmark-dark-400x119.png
├── robots.txt
├── index.php
├── js
│ └── filament
│ │ └── forms
│ │ └── components
│ │ ├── textarea.js
│ │ ├── tags-input.js
│ │ └── key-value.js
├── .htaccess
├── vendor
│ └── feed
│ │ └── style.css
└── css
│ └── app
│ └── spatie-markdown-editor.css
├── database
├── .gitignore
├── settings
│ ├── 2025_08_06_230502_create_general_settings.php
│ ├── 2025_11_09_000000_add_extended_seo_settings.php
│ ├── 2025_08_08_215627_create_social_accounts_settings.php
│ └── 2025_08_06_233023_create_seo_settings.php
├── seeders
│ └── DatabaseSeeder.php
├── migrations
│ ├── 2022_12_14_083707_create_settings_table.php
│ ├── 2025_03_06_003846_alter_indexes_table.php
│ ├── 2025_07_15_192921_add_is_featured_to_packages_table.php
│ ├── 2025_03_05_181441_add_is_admin_to_users_table.php
│ ├── 2025_07_29_210517_add_notification_sent_to_package_submissions_table.php
│ ├── 2025_08_01_183554_create_newsletter_subscribers_table.php
│ ├── 2025_03_08_231823_create_index_package_table.php
│ ├── 2025_02_12_191127_create_admins_table.php
│ ├── 2025_07_20_153252_add_user_id_to_packages_table.php
│ ├── 2025_08_01_231351_alter_user_agent_length_in_blog_post_views_table.php
│ ├── 2025_03_09_021616_make_index_id_nullable_in_packages_table.php
│ ├── 2025_02_18_032836_create_category_package_table.php
│ ├── 2025_12_08_222333_create_feed_post_bookmarks_table.php
│ ├── 2025_03_15_222014_create_blog_post_category_table.php
│ ├── 2025_07_27_200031_create_package_submissions_table.php
│ ├── 2025_12_08_191651_create_feed_comment_user_table.php
│ ├── 0001_01_01_000001_create_cache_table.php
│ ├── 2025_02_28_194503_create_personal_access_tokens_table.php
│ ├── 2025_12_08_191403_create_feed_post_user_table.php
│ ├── 2025_02_14_202654_create_indexes_table.php
│ ├── 2025_12_08_222318_create_feed_post_votes_table.php
│ ├── 2025_02_18_001104_create_categories_table.php
│ ├── 2025_07_20_151912_add_username_to_users_table.php
│ ├── 2025_02_14_202331_create_media_table.php
│ ├── 2025_12_08_191402_create_feed_comments_table.php
│ ├── 2025_04_04_140517_create_blog_post_views_table.php
│ ├── 2025_07_19_151100_add_social_auth_fields_to_users_table.php
│ ├── 2025_03_15_221709_create_blog_posts_table.php
│ ├── 2025_12_08_191329_create_feed_sources_table.php
│ └── 2025_02_18_030230_create_packages_table.php
└── factories
│ └── UserFactory.php
├── resources
├── js
│ ├── types
│ │ ├── globals.d.ts
│ │ ├── vite-env.d.ts
│ │ └── global.d.ts
│ ├── bootstrap.ts
│ ├── hooks
│ │ ├── use-slugify.ts
│ │ ├── use-click-tracker.ts
│ │ ├── use-forwarded-ref.ts
│ │ ├── use-mobile.tsx
│ │ └── use-image-upload.ts
│ ├── components
│ │ ├── ui
│ │ │ ├── collapsible.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── floating-element.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── badge.tsx
│ │ │ └── popover.tsx
│ │ ├── shared
│ │ │ └── AppHead.tsx
│ │ ├── mode-toggle.tsx
│ │ ├── input-error.tsx
│ │ ├── input-label.tsx
│ │ ├── checkbox.tsx
│ │ ├── single-select.tsx
│ │ ├── danger-button.tsx
│ │ ├── multi-select.tsx
│ │ ├── primary-button.tsx
│ │ ├── nav-link.tsx
│ │ ├── secondary-button.tsx
│ │ ├── text-input.tsx
│ │ └── responsive-nav-link.tsx
│ ├── lib
│ │ └── utils.ts
│ ├── Layouts
│ │ └── GuestLayout.tsx
│ ├── ssr.tsx
│ ├── app.tsx
│ └── Pages
│ │ └── Admin
│ │ └── Profile
│ │ └── Edit.tsx
├── views
│ ├── vendor
│ │ ├── mail
│ │ │ ├── text
│ │ │ │ ├── footer.blade.php
│ │ │ │ ├── panel.blade.php
│ │ │ │ ├── subcopy.blade.php
│ │ │ │ ├── table.blade.php
│ │ │ │ ├── button.blade.php
│ │ │ │ ├── header.blade.php
│ │ │ │ ├── layout.blade.php
│ │ │ │ └── message.blade.php
│ │ │ └── html
│ │ │ │ ├── table.blade.php
│ │ │ │ ├── subcopy.blade.php
│ │ │ │ ├── header.blade.php
│ │ │ │ ├── footer.blade.php
│ │ │ │ ├── panel.blade.php
│ │ │ │ ├── message.blade.php
│ │ │ │ └── button.blade.php
│ │ └── feed
│ │ │ └── links.blade.php
│ ├── filament
│ │ ├── pages
│ │ │ └── analytics.blade.php
│ │ └── widgets
│ │ │ └── blog-analytics-stats.blade.php
│ └── errors
│ │ └── 419.blade.php
└── css
│ └── sonner-overrides.css
├── bootstrap
├── cache
│ └── .gitignore
└── providers.php
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── private
│ │ └── .gitignore
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
├── debugbar
│ └── .gitignore
├── framework
│ ├── testing
│ │ └── .gitignore
│ ├── views
│ │ └── .gitignore
│ ├── cache
│ │ ├── data
│ │ │ └── .gitignore
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ └── .gitignore
└── model_status_installed.lock
├── postcss.config.js
├── app
├── Http
│ ├── Controllers
│ │ ├── Controller.php
│ │ ├── Auth
│ │ │ ├── Social
│ │ │ │ ├── GithubRedirectController.php
│ │ │ │ └── GithubCallbackController.php
│ │ │ ├── EmailVerificationPromptController.php
│ │ │ ├── EmailVerificationNotificationController.php
│ │ │ ├── PasswordController.php
│ │ │ ├── VerifyEmailController.php
│ │ │ ├── PasswordResetLinkController.php
│ │ │ └── ConfirmablePasswordController.php
│ │ ├── OgImageController.php
│ │ ├── Api
│ │ │ ├── SearchController.php
│ │ │ ├── FeedController.php
│ │ │ └── BookmarkController.php
│ │ ├── Admin
│ │ │ ├── DashboardController.php
│ │ │ ├── GetPackageRepoDataController.php
│ │ │ ├── Auth
│ │ │ │ ├── PasswordController.php
│ │ │ │ ├── ForgotPasswordController.php
│ │ │ │ ├── LoginController.php
│ │ │ │ └── ResetPasswordController.php
│ │ │ ├── ToggleStatusController.php
│ │ │ └── ProfileController.php
│ │ ├── NewsletterSubscriptionController.php
│ │ ├── Application
│ │ │ └── Feed
│ │ │ │ ├── TogglePostBookmarkController.php
│ │ │ │ ├── FeedController.php
│ │ │ │ ├── TogglePostVoteController.php
│ │ │ │ ├── ToggleCommentVoteController.php
│ │ │ │ ├── BookmarkController.php
│ │ │ │ ├── FeedPostController.php
│ │ │ │ └── FeedCommentController.php
│ │ ├── UserController.php
│ │ ├── Profile
│ │ │ ├── ProfileSecurityController.php
│ │ │ └── ProfileInformationController.php
│ │ └── BlogController.php
│ ├── Requests
│ │ ├── Admin
│ │ │ ├── GetPackageRepoDataRequest.php
│ │ │ ├── ProfileUpdateRequest.php
│ │ │ ├── CreateIndexRequest.php
│ │ │ └── UpdateIndexRequest.php
│ │ ├── SecurityUpdateRequest.php
│ │ ├── Application
│ │ │ └── Feed
│ │ │ │ ├── ToggleVoteRequest.php
│ │ │ │ └── CreateCommentRequest.php
│ │ ├── ProfileUpdateRequest.php
│ │ ├── CreateNewsletterSubscriptionRequest.php
│ │ └── CreatePackageSubmissionRequest.php
│ ├── Resources
│ │ ├── Application
│ │ │ └── Feed
│ │ │ │ ├── FeedSourceResource.php
│ │ │ │ └── FeedCommentResource.php
│ │ ├── IndexResource.php
│ │ ├── CategoryResource.php
│ │ ├── UserResource.php
│ │ ├── Admin
│ │ │ └── IndexResource.php
│ │ ├── BlogPostResource.php
│ │ ├── PackageSubmissionResource.php
│ │ └── PackageResource.php
│ └── Middleware
│ │ └── AdminAuthMiddleware.php
├── Enums
│ ├── SocialProvider.php
│ └── ReviewStatus.php
├── Traits
│ └── HasStatus.php
├── Support
│ └── AdminDetector.php
├── Settings
│ ├── GeneralSettings.php
│ ├── SocialAccountsSettings.php
│ └── SeoSettings.php
├── View
│ └── Components
│ │ └── vendor
│ │ └── feed
│ │ └── FeedLinks.php
├── Queries
│ ├── MostReadPostsThisWeekQuery.php
│ ├── AvatarUsersMapQuery.php
│ ├── LatestPublishedPostsQuery.php
│ └── PackagesOrderedByFeaturedQuery.php
├── Filament
│ ├── Resources
│ │ ├── IndexResource
│ │ │ └── Pages
│ │ │ │ ├── ViewIndex.php
│ │ │ │ ├── CreateIndex.php
│ │ │ │ ├── EditIndex.php
│ │ │ │ └── ListIndices.php
│ │ ├── UserResource
│ │ │ └── Pages
│ │ │ │ ├── CreateUser.php
│ │ │ │ ├── EditUser.php
│ │ │ │ ├── ViewUser.php
│ │ │ │ └── ListUsers.php
│ │ ├── BlogPostResource
│ │ │ └── Pages
│ │ │ │ ├── CreateBlogPost.php
│ │ │ │ ├── EditBlogPost.php
│ │ │ │ └── ListBlogPosts.php
│ │ ├── FeedSourceResource
│ │ │ └── Pages
│ │ │ │ ├── CreateFeedSource.php
│ │ │ │ ├── EditFeedSource.php
│ │ │ │ └── ListFeedSources.php
│ │ ├── CategoryResource
│ │ │ └── Pages
│ │ │ │ └── ViewPackageCategory.php
│ │ ├── BlogPostCategoryResource
│ │ │ └── Pages
│ │ │ │ ├── ViewBlogPostCategory.php
│ │ │ │ ├── CreateBlogPostCategory.php
│ │ │ │ ├── EditBlogPostCategory.php
│ │ │ │ └── ListBlogPostCategories.php
│ │ ├── PackageCategoryResource
│ │ │ └── Pages
│ │ │ │ ├── CreatePackageCategory.php
│ │ │ │ ├── EditPackageCategory.php
│ │ │ │ └── ListPackageCategories.php
│ │ ├── NewsletterSubscriberResource
│ │ │ └── Pages
│ │ │ │ ├── ViewNewsletterSubscriber.php
│ │ │ │ └── ListNewsletterSubscribers.php
│ │ ├── PackageSubmissionResource
│ │ │ └── Pages
│ │ │ │ └── ListPackageSubmissions.php
│ │ └── PackageResource
│ │ │ └── Pages
│ │ │ └── CreatePackage.php
│ ├── Widgets
│ │ └── StatsOverview.php
│ └── Pages
│ │ ├── Settings
│ │ └── GeneralSettings.php
│ │ ├── Analytics.php
│ │ └── ManageSettings.php
├── Models
│ ├── BlogPostView.php
│ ├── Admin.php
│ ├── NewsletterSubscriber.php
│ ├── PackageSubmission.php
│ ├── FeedPostBookmark.php
│ ├── FeedPostVote.php
│ └── Scopes
│ │ └── PublishedScope.php
├── ModelFilters
│ ├── FeedPostFilter.php
│ └── PackageFilter.php
├── Actions
│ ├── CreatePackageSubmissionAction.php
│ ├── CreateNewsletterSubscriptionAction.php
│ ├── Feed
│ │ └── TogglePostBookmarkAction.php
│ └── SendPasswordResetLinkAction.php
├── Services
│ ├── MixpanelService.php
│ └── UserService.php
├── Rules
│ └── AdminCurrentPassword.php
├── Console
│ └── Commands
│ │ ├── PublishScheduledPosts.php
│ │ ├── RelateIndexesToPackages.php
│ │ └── FixSequences.php
├── Providers
│ └── HorizonServiceProvider.php
├── Jobs
│ ├── GenerateOgImageForPackageJob.php
│ └── RecordBlogPostViewsJob.php
└── Notifications
│ └── NewsletterWelcomeNotification.php
├── pint.json
├── tests
├── TestCase.php
└── Feature
│ └── Auth
│ ├── RegistrationTest.php
│ └── PasswordConfirmationTest.php
├── .gitattributes
├── routes
├── console.php
└── api.php
├── config
├── filament-page-with-sidebar.php
└── services.php
├── .editorconfig
├── vite.config.js
├── artisan
├── .gitignore
├── components.json
├── lang
└── en
│ ├── pagination.php
│ ├── auth.php
│ └── passwords.php
├── tsconfig.json
├── .prettierignore
├── .prettierrc
├── LICENSE
└── phpunit.xml
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/resources/js/types/globals.d.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/private/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/debugbar/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/model_status_installed.lock:
--------------------------------------------------------------------------------
1 | 2025-02-17 20:24:34
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/footer.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/panel.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/subcopy.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/table.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}
2 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !private/
3 | !public/
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/resources/js/types/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/button.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}: {{ $url }}
2 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/header.blade.php:
--------------------------------------------------------------------------------
1 | {{ $slot }}: {{ $url }}
2 |
--------------------------------------------------------------------------------
/public/assets/images/419.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/419.png
--------------------------------------------------------------------------------
/public/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/logo.png
--------------------------------------------------------------------------------
/public/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/favicon.ico
--------------------------------------------------------------------------------
/public/assets/images/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/og-image.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/images/logo-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/logo-icon.png
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/table.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }}
3 |
4 |
--------------------------------------------------------------------------------
/public/assets/images/github-avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheLaravelHub/laravel-hub/HEAD/public/assets/images/github-avatar.jpg
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | |
4 | {{ Illuminate\Mail\Markdown::parse($slot) }}
5 | |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/layout.blade.php:
--------------------------------------------------------------------------------
1 | {!! strip_tags($header ?? '') !!}
2 |
3 | {!! strip_tags($slot) !!}
4 | @isset($subcopy)
5 |
6 | {!! strip_tags($subcopy) !!}
7 | @endisset
8 |
9 | {!! strip_tags($footer ?? '') !!}
10 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
3 | *.blade.php diff=html
4 | *.css diff=css
5 | *.html diff=html
6 | *.md diff=markdown
7 | *.php diff=php
8 |
9 | /.github export-ignore
10 | CHANGELOG.md export-ignore
11 | .styleci.yml export-ignore
12 |
--------------------------------------------------------------------------------
/app/Traits/HasStatus.php:
--------------------------------------------------------------------------------
1 | attributes['status'] = $value ? 'active' : 'inactive';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
8 | })->purpose('Display an inspiring quote');
9 |
--------------------------------------------------------------------------------
/bootstrap/providers.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | @foreach ($this->getVisibleWidgets() as $widget)
4 | @livewire($widget)
5 | @endforeach
6 |
7 |
8 |
--------------------------------------------------------------------------------
/resources/views/vendor/feed/links.blade.php:
--------------------------------------------------------------------------------
1 | @foreach($feeds as $name => $feed)
2 |
3 | @endforeach
4 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/header.blade.php:
--------------------------------------------------------------------------------
1 | @props(['url'])
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app/Support/AdminDetector.php:
--------------------------------------------------------------------------------
1 | is_admin ?? false);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/config/filament-page-with-sidebar.php:
--------------------------------------------------------------------------------
1 | [
8 | '2xl' => '3',
9 | 'xl' => '3',
10 | 'lg' => '3',
11 | 'md' => '3',
12 | 'sm' => '12',
13 | ],
14 | ];
15 |
--------------------------------------------------------------------------------
/resources/js/hooks/use-slugify.ts:
--------------------------------------------------------------------------------
1 | const useSlugify = () => {
2 | const slugify = (str: string) => {
3 | return str
4 | .toLowerCase()
5 | .replace(/ /g, '-')
6 | .replace(/[^\w-]+/g, '')
7 | }
8 | return { slugify }
9 | }
10 |
11 | export default useSlugify
12 |
--------------------------------------------------------------------------------
/app/Settings/GeneralSettings.php:
--------------------------------------------------------------------------------
1 | migrator->add('general.site_name', env('APP_NAME'));
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/footer.blade.php:
--------------------------------------------------------------------------------
1 |
2 | |
3 |
10 | |
11 |
12 |
--------------------------------------------------------------------------------
/app/Queries/MostReadPostsThisWeekQuery.php:
--------------------------------------------------------------------------------
1 | {
5 | return useCallback(() => {
6 | Mixpanel.track(type, options)
7 | }, [type, options])
8 | }
9 |
10 | export default useClickTracker
11 |
--------------------------------------------------------------------------------
/app/Models/BlogPostView.php:
--------------------------------------------------------------------------------
1 | belongsTo(BlogPost::class);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostResource/Pages/CreateBlogPost.php:
--------------------------------------------------------------------------------
1 | query->whereAny(['title', 'excerpt'], 'ILIKE', "%{$query}%");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/resources/js/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/resources/js/components/shared/AppHead.tsx:
--------------------------------------------------------------------------------
1 | import { Head } from '@inertiajs/react'
2 |
3 | export default function AppHead({
4 | title,
5 | children,
6 | }: {
7 | title?: string
8 | children: React.ReactNode
9 | }) {
10 | return (
11 |
12 | {title ? title : 'Laravel Hub'}
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import laravel from 'laravel-vite-plugin'
3 | import react from '@vitejs/plugin-react'
4 |
5 | export default defineConfig({
6 | plugins: [
7 | laravel({
8 | input: 'resources/js/app.tsx',
9 | ssr: 'resources/js/ssr.tsx',
10 | refresh: true,
11 | }),
12 | react(),
13 | ],
14 | })
15 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostCategoryResource/Pages/ViewBlogPostCategory.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | |
7 | {{ Illuminate\Mail\Markdown::parse($slot) }}
8 | |
9 |
10 |
11 | |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostCategoryResource/Pages/CreateBlogPostCategory.php:
--------------------------------------------------------------------------------
1 | handleCommand(new ArgvInput);
14 |
15 | exit($status);
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.phpunit.cache
2 | /bootstrap/ssr
3 | /node_modules
4 | /public/build
5 | /public/hot
6 | /public/storage
7 | /storage/*.key
8 | /storage/pail
9 | /vendor
10 | .env
11 | .env.backup
12 | .env.production
13 | .phpactor.json
14 | .phpunit.result.cache
15 | Homestead.json
16 | Homestead.yaml
17 | npm-debug.log
18 | yarn-error.log
19 | /auth.json
20 | /.fleet
21 | /.idea
22 | /.nova
23 | /.vscode
24 | /.zed
25 | .env.testing
26 |
--------------------------------------------------------------------------------
/app/Filament/Resources/NewsletterSubscriberResource/Pages/ViewNewsletterSubscriber.php:
--------------------------------------------------------------------------------
1 | redirect();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/Http/Controllers/OgImageController.php:
--------------------------------------------------------------------------------
1 |
11 |
12 | Light mode
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/resources/js/components/input-error.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react'
2 |
3 | export default function InputError({
4 | message,
5 | className = '',
6 | ...props
7 | }: HTMLAttributes & { message?: string }) {
8 | return message ? (
9 |
13 | {message}
14 |
15 | ) : null
16 | }
17 |
--------------------------------------------------------------------------------
/app/Models/Admin.php:
--------------------------------------------------------------------------------
1 | 'hashed',
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/resources/js/hooks/use-forwarded-ref.ts:
--------------------------------------------------------------------------------
1 | import type React from 'react'
2 | import { useEffect, useRef } from 'react'
3 |
4 | export function useForwardedRef(ref: React.ForwardedRef) {
5 | const innerRef = useRef(null)
6 |
7 | useEffect(() => {
8 | if (!ref) return
9 | if (typeof ref === 'function') {
10 | ref(innerRef.current)
11 | } else {
12 | ref.current = innerRef.current
13 | }
14 | })
15 |
16 | return innerRef
17 | }
18 |
--------------------------------------------------------------------------------
/app/ModelFilters/PackageFilter.php:
--------------------------------------------------------------------------------
1 | whereRaw('LOWER(language) = ?', [strtolower($lang)]);
12 | }
13 |
14 | public function isFeatured(bool $isFeatured = false)
15 | {
16 | if ($isFeatured) {
17 | return $this->orderByDesc('is_featured');
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Filament/Resources/UserResource/Pages/EditUser.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | protected $casts = [
20 | 'is_active' => 'boolean',
21 | ];
22 | }
23 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostResource/Pages/EditBlogPost.php:
--------------------------------------------------------------------------------
1 | handleRequest(Request::capture());
18 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostResource/Pages/ListBlogPosts.php:
--------------------------------------------------------------------------------
1 | whereNotNull('avatar')
13 | ->limit(6)
14 | ->get(['id', 'name', 'avatar'])
15 | ->keyBy('id')
16 | ->map(fn ($u) => [
17 | 'name' => $u->name,
18 | 'avatar' => $u->avatar,
19 | ])
20 | ->toArray();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Filament/Resources/FeedSourceResource/Pages/ListFeedSources.php:
--------------------------------------------------------------------------------
1 | published()
14 | ->select(['id', 'title', 'sub_title', 'slug', 'published_at', 'meta_description'])
15 | ->with('categories:id,name,slug')
16 | ->latest('published_at')
17 | ->limit(6)
18 | ->get();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/components/input-label.tsx:
--------------------------------------------------------------------------------
1 | import { LabelHTMLAttributes } from 'react'
2 |
3 | export default function InputLabel({
4 | value,
5 | className = '',
6 | children,
7 | ...props
8 | }: LabelHTMLAttributes & { value?: string }) {
9 | return (
10 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/Filament/Resources/BlogPostCategoryResource/Pages/EditBlogPostCategory.php:
--------------------------------------------------------------------------------
1 | select(['id', 'name', 'slug', 'description', 'stars', 'owner', 'owner_avatar'])
14 | ->filter(['is_featured' => true])
15 | ->with('categories:id,name,slug')
16 | ->orderByDesc('stars')
17 | ->limit(6)
18 | ->get();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Filament/Resources/NewsletterSubscriberResource/Pages/ListNewsletterSubscribers.php:
--------------------------------------------------------------------------------
1 | input('term')) ?? '')
17 | ->get();
18 |
19 | $packages->load('categories');
20 |
21 | return response()->json($packages);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Models/PackageSubmission.php:
--------------------------------------------------------------------------------
1 | 'boolean',
15 | ];
16 |
17 | /**
18 | * Relationship: PackageSubmission belongs to a User
19 | */
20 | public function user(): BelongsTo
21 | {
22 | return $this->belongsTo(User::class);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "resources/css/app.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/resources/js/components/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { InputHTMLAttributes } from 'react'
2 |
3 | export default function Checkbox({
4 | className = '',
5 | ...props
6 | }: InputHTMLAttributes) {
7 | return (
8 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | name('search')
10 | ->middleware('throttle:60,1');
11 |
12 | Route::get('feed', [FeedController::class, 'index'])
13 | ->name('api.feed')
14 | ->middleware(['web', 'auth']);
15 |
16 | Route::get('bookmarks', [BookmarkController::class, 'index'])
17 | ->name('api.bookmarks')
18 | ->middleware(['web', 'auth']);
19 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/GetPackageRepoDataRequest.php:
--------------------------------------------------------------------------------
1 | |string>
13 | */
14 | public function rules(): array
15 | {
16 | return [
17 | 'repository_url' => ['required', 'url', 'regex:/github\.com\/([^\/]+)\/([^\/]+)/'],
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/app/Http/Resources/Application/Feed/FeedSourceResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
16 | 'name' => $this->name,
17 | 'slug' => $this->slug,
18 | 'logo' => $this->logo_url,
19 | 'type' => $this->type,
20 | ];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Models/FeedPostBookmark.php:
--------------------------------------------------------------------------------
1 | belongsTo(FeedPost::class);
20 | }
21 |
22 | public function user(): BelongsTo
23 | {
24 | return $this->belongsTo(User::class);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/settings/2025_11_09_000000_add_extended_seo_settings.php:
--------------------------------------------------------------------------------
1 | migrator->add('seo.og_image', null);
10 | $this->migrator->add('seo.og_site_name', 'Laravel Hub');
11 | $this->migrator->add('seo.og_locale', 'en_US');
12 | $this->migrator->add('seo.twitter_card', 'summary_large_image');
13 | $this->migrator->add('seo.twitter_site', '@thelaravelhub');
14 | $this->migrator->add('seo.twitter_image', null);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/app/Actions/CreatePackageSubmissionAction.php:
--------------------------------------------------------------------------------
1 | $data
15 | */
16 | public function handle(User $user, array $data): PackageSubmission
17 | {
18 | return $user->packageSubmissions()->create([
19 | 'repository_url' => $data['repository_url'],
20 | 'status' => ReviewStatus::PENDING,
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Models/FeedPostVote.php:
--------------------------------------------------------------------------------
1 | belongsTo(FeedPost::class);
21 | }
22 |
23 | public function user(): BelongsTo
24 | {
25 | return $this->belongsTo(User::class);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/settings/2025_08_08_215627_create_social_accounts_settings.php:
--------------------------------------------------------------------------------
1 | migrator->add('social-accounts.github', null);
10 | $this->migrator->add('social-accounts.x', null);
11 | $this->migrator->add('social-accounts.facebook', null);
12 | $this->migrator->add('social-accounts.telegram', null);
13 | $this->migrator->add('social-accounts.bluesky', null);
14 | $this->migrator->add('social-accounts.linkedin', null);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/message.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{-- Header --}}
3 |
4 |
5 | {{ config('app.name') }}
6 |
7 |
8 |
9 | {{-- Body --}}
10 | {!! $slot !!}
11 |
12 | {{-- Subcopy --}}
13 | @isset($subcopy)
14 |
15 |
16 | {!! $subcopy !!}
17 |
18 |
19 | @endisset
20 |
21 | {{-- Footer --}}
22 |
23 |
24 | © {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | User::factory()->create([
19 | 'name' => 'Admin',
20 | 'email' => 'admin@admin.dev',
21 | 'password' => bcrypt('password'),
22 | 'is_admin' => true,
23 | ]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/DashboardController.php:
--------------------------------------------------------------------------------
1 | active()->count();
15 | $inActivePackagesCount = Package::query()->inActive()->count();
16 |
17 | return Inertia::render('Admin/User/Index',
18 | compact('packagesCount', 'activePackagesCount', 'inActivePackagesCount'));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/public/js/filament/forms/components/textarea.js:
--------------------------------------------------------------------------------
1 | function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init:function(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{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 |
--------------------------------------------------------------------------------
/app/Http/Controllers/NewsletterSubscriptionController.php:
--------------------------------------------------------------------------------
1 | handle($request->validated());
15 |
16 | return Redirect::back()->with('success', 'Thanks for subscribing!');
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Requests/SecurityUpdateRequest.php:
--------------------------------------------------------------------------------
1 | |string>
14 | */
15 | public function rules(): array
16 | {
17 | return [
18 | 'current_password' => ['required', 'current_password'],
19 | 'password' => ['required', Password::defaults(), 'confirmed'],
20 | ];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Services/MixpanelService.php:
--------------------------------------------------------------------------------
1 | mixpanel = Mixpanel::getInstance(env('MIXPANEL_TOKEN'));
14 | }
15 |
16 | public function trackEvent(string $eventName, array $properties = []): void
17 | {
18 | $this->mixpanel->track($eventName, $properties);
19 | }
20 |
21 | public function identifyUser(string $distinctId, array $properties = []): void
22 | {
23 | $this->mixpanel->identify($distinctId);
24 | $this->mixpanel->people->set($distinctId, $properties);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/html/button.blade.php:
--------------------------------------------------------------------------------
1 | @props([
2 | 'url',
3 | 'color' => 'primary',
4 | 'align' => 'center',
5 | ])
6 |
7 |
8 |
9 |
10 |
11 | |
12 |
19 | |
20 |
21 |
22 | |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/Http/Middleware/AdminAuthMiddleware.php:
--------------------------------------------------------------------------------
1 | guard('admin')->check()) {
19 | return redirect()->route('admin.login')->with('error', 'Please log in as an admin.');
20 | }
21 |
22 | return $next($request);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/migrations/2022_12_14_083707_create_settings_table.php:
--------------------------------------------------------------------------------
1 | id();
13 |
14 | $table->string('group');
15 | $table->string('name');
16 | $table->boolean('locked')->default(false);
17 | $table->json('payload');
18 |
19 | $table->timestamps();
20 |
21 | $table->unique(['group', 'name']);
22 | });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/resources/js/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(
7 | undefined,
8 | )
9 |
10 | React.useEffect(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
12 | const onChange = () => {
13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
14 | }
15 | mql.addEventListener('change', onChange)
16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
17 | return () => mql.removeEventListener('change', onChange)
18 | }, [])
19 |
20 | return !!isMobile
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "jsx": "react-jsx",
7 | "strict": true,
8 | "isolatedModules": true,
9 | "target": "ESNext",
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "skipLibCheck": true,
13 | "noEmit": true,
14 | "paths": {
15 | "@/*": ["./resources/js/*"],
16 | "ziggy-js": ["./vendor/tightenco/ziggy"]
17 | }
18 | },
19 | "include": [
20 | "resources/js/**/*.ts",
21 | "resources/js/**/*.tsx",
22 | "resources/js/**/*.d.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/app/Actions/CreateNewsletterSubscriptionAction.php:
--------------------------------------------------------------------------------
1 | $data
14 | */
15 | public function handle(array $data): NewsletterSubscriber
16 | {
17 | $subscriber = NewsletterSubscriber::create([
18 | 'email' => $data['email'],
19 | 'is_active' => true,
20 | ]);
21 | $subscriber->notify(new NewsletterWelcomeNotification);
22 |
23 | return $subscriber;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Services/UserService.php:
--------------------------------------------------------------------------------
1 | lower()
14 | ->replaceMatches('/[^a-z0-9]+/', '.')
15 | ->trim('.');
16 |
17 | $username = $base;
18 | $i = 1;
19 |
20 | while (
21 | DB::table('users')
22 | ->where('username', $username)
23 | ->exists()
24 | ) {
25 | $username = "{$base}{$i}";
26 | $i++;
27 | }
28 |
29 | return $username;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'password' => 'The provided password is incorrect.',
18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/app/Models/Scopes/PublishedScope.php:
--------------------------------------------------------------------------------
1 | is_admin) {
19 | $builder->where('status', 'published')
20 | ->where('published_at', '<=', now());
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/Social/GithubCallbackController.php:
--------------------------------------------------------------------------------
1 | user();
16 |
17 | $user = $action->handle('github', $githubUser);
18 |
19 | Auth::login($user);
20 |
21 | return redirect()
22 | ->route('app.feed.home');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationPromptController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()
19 | ? redirect()->intended(route('app.feed.home', absolute: false))
20 | : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Requests/Application/Feed/ToggleVoteRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'vote_type' => 'required|in:upvote,downvote',
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Resources/IndexResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | return [
18 | 'id' => $this->id,
19 | 'name' => $this->name,
20 | 'description' => $this->description,
21 | 'slug' => $this->slug,
22 | 'icon' => $this->getFirstMediaUrl(),
23 | 'color_code' => $this->color_code,
24 | 'status' => $this->status,
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/settings/2025_08_06_233023_create_seo_settings.php:
--------------------------------------------------------------------------------
1 | migrator->add('seo.title', env('APP_NAME'));
10 | $this->migrator->add('seo.description', null);
11 | $this->migrator->add('seo.keywords', null);
12 | $this->migrator->add('seo.og_title', null);
13 | $this->migrator->add('seo.og_description', null);
14 | $this->migrator->add('seo.og_url', null);
15 | $this->migrator->add('seo.og_type', 'website');
16 | $this->migrator->add('seo.twitter_title', null);
17 | $this->migrator->add('seo.twitter_description', null);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationNotificationController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
17 | return redirect()->intended(route('app.feed.home', absolute: false));
18 | }
19 |
20 | $request->user()->sendEmailVerificationNotification();
21 |
22 | return back()->with('status', 'verification-link-sent');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Rules/AdminCurrentPassword.php:
--------------------------------------------------------------------------------
1 | password)) {
22 | $fail(__('The current password is incorrect.'));
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/views/vendor/mail/text/message.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{-- Header --}}
3 |
4 |
5 | {{ config('app.name') }}
6 |
7 |
8 |
9 | {{-- Body --}}
10 | {{ $slot }}
11 |
12 | {{-- Subcopy --}}
13 | @isset($subcopy)
14 |
15 |
16 | {{ $subcopy }}
17 |
18 |
19 | @endisset
20 |
21 | {{-- Footer --}}
22 |
23 |
24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/Http/Resources/CategoryResource.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | public function toArray(Request $request): array
18 | {
19 | return [
20 | 'id' => $this->id,
21 | 'name' => $this->name,
22 | 'slug' => $this->slug,
23 | 'meta_title' => $this->meta_title,
24 | 'meta_description' => $this->meta_description,
25 | 'packages_count' => $this->packages_count,
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_06_003846_alter_indexes_table.php:
--------------------------------------------------------------------------------
1 | text('description')->nullable()->change();
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 |
25 | Schema::table('indexes', function (Blueprint $table) {
26 | $table->text('description')->nullable(false)->change();
27 | });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/resources/css/sonner-overrides.css:
--------------------------------------------------------------------------------
1 | /* Custom overrides for sonner toast notifications */
2 | :root {
3 | --toast-icon-margin: 0;
4 | --toast-height: auto !important;
5 | --normal-height: auto !important;
6 | }
7 |
8 | [data-sonner-toaster] {
9 | height: auto !important;
10 | max-height: 100vh !important;
11 | }
12 |
13 | [data-sonner-toast] {
14 | height: auto !important;
15 | min-height: 0 !important;
16 | display: flex !important;
17 | align-items: center !important;
18 | padding: 8px 12px !important;
19 | }
20 |
21 | [data-sonner-toast] [data-icon] {
22 | margin-right: 8px !important;
23 | display: inline-flex !important;
24 | }
25 |
26 | [data-sonner-toast] [data-title] {
27 | display: inline-flex !important;
28 | align-items: center !important;
29 | }
30 |
--------------------------------------------------------------------------------
/resources/js/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { twMerge } from 'tailwind-merge'
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export function capitalizeWords(str: string): string {
9 | return str.replace(/\b\w/g, (char) => char.toUpperCase())
10 | }
11 |
12 | export const formatNumber = (num: number) => {
13 | if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M'
14 | if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K'
15 | return num.toString()
16 | }
17 |
18 | export function formatDate(date: string | Date): string {
19 | const d = new Date(date)
20 | return d.toLocaleDateString('en-US', {
21 | year: 'numeric',
22 | month: 'long',
23 | day: 'numeric',
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Application/Feed/TogglePostBookmarkController.php:
--------------------------------------------------------------------------------
1 | execute($feedPost, $request->user());
16 |
17 | return response()->json([
18 | 'is_bookmarked' => $result['action'] === 'added',
19 | 'bookmarks_count' => $result['bookmarks_count'],
20 | 'action' => $result['action'],
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/GetPackageRepoDataController.php:
--------------------------------------------------------------------------------
1 | repository_url);
19 |
20 | return response()->json($repositoryData);
21 | } catch (\Throwable $th) {
22 | return response()->json(['error' => $th->getMessage()], 500);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/database/migrations/2025_07_15_192921_add_is_featured_to_packages_table.php:
--------------------------------------------------------------------------------
1 | boolean('is_featured')
16 | ->default(false);
17 | });
18 | }
19 |
20 | /**
21 | * Reverse the migrations.
22 | */
23 | public function down(): void
24 | {
25 | Schema::table('packages', function (Blueprint $table) {
26 | $table->dropColumn('is_featured');
27 | });
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Application/Feed/FeedController.php:
--------------------------------------------------------------------------------
1 | all())
17 | ->with(['source', 'comments.user', 'comments.replies.user'])
18 | ->latest('published_at')
19 | ->cursorPaginate(20);
20 |
21 | return Inertia::render('User/Feed', [
22 | 'posts' => FeedPostResource::collection($posts),
23 | ]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/UserController.php:
--------------------------------------------------------------------------------
1 | whereId($request->user()->id)->withCount('packages')->first();
19 | $popularBlogPosts = BlogPost::popularThisWeek();
20 |
21 | return inertia('User/Index', [
22 | 'user' => UserResource::make($user),
23 | 'popularBlogPosts' => BlogPostResource::collection($popularBlogPosts),
24 | ]);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/js/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<'textarea'>
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = 'Textarea'
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_05_181441_add_is_admin_to_users_table.php:
--------------------------------------------------------------------------------
1 | boolean('is_admin')
16 | ->after('status')
17 | ->default(false);
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | */
24 | public function down(): void
25 | {
26 | Schema::table('users', function (Blueprint $table) {
27 | $table->dropColumn('is_admin');
28 | });
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/resources/js/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | },
19 | )
20 | Input.displayName = 'Input'
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/lang/en/passwords.php:
--------------------------------------------------------------------------------
1 | 'Your password has been reset.',
17 | 'sent' => 'We have emailed your password reset link.',
18 | 'throttled' => 'Please wait before retrying.',
19 | 'token' => 'This password reset token is invalid.',
20 | 'user' => "We can't find a user with that email address.",
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Application/Feed/TogglePostVoteController.php:
--------------------------------------------------------------------------------
1 | execute($feedPost, $request->user(), $request->vote_type);
15 |
16 | return response()->json([
17 | 'upvotes' => $result['upvotes_count'],
18 | 'downvotes' => $result['downvotes_count'],
19 | 'action' => $result['action'],
20 | ]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Requests/Application/Feed/CreateCommentRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'content' => 'required|string|max:5000',
26 | 'parent_id' => 'nullable|exists:feed_comments,id',
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/database/migrations/2025_07_29_210517_add_notification_sent_to_package_submissions_table.php:
--------------------------------------------------------------------------------
1 | boolean('notification_sent')->default(false);
16 | });
17 | }
18 |
19 | /**
20 | * Reverse the migrations.
21 | */
22 | public function down(): void
23 | {
24 | Schema::table('package_submissions', function (Blueprint $table) {
25 | $table->dropColumn('notification_sent');
26 | });
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Handle X-XSRF-Token Header
13 | RewriteCond %{HTTP:x-xsrf-token} .
14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
15 |
16 | # Redirect Trailing Slashes If Not A Folder...
17 | RewriteCond %{REQUEST_FILENAME} !-d
18 | RewriteCond %{REQUEST_URI} (.+)/$
19 | RewriteRule ^ %1 [L,R=301]
20 |
21 | # Send Requests To Front Controller...
22 | RewriteCond %{REQUEST_FILENAME} !-d
23 | RewriteCond %{REQUEST_FILENAME} !-f
24 | RewriteRule ^ index.php [L]
25 |
26 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore Node.js dependencies
2 | node_modules
3 | package-lock.json
4 | pnpm-lock.yaml
5 | yarn.lock
6 |
7 | # Ignore build files
8 | dist
9 | build
10 | public/storage
11 | public/css/*
12 | public/js/*
13 | storage
14 | bootstrap/cache
15 |
16 | # Ignore Laravel-specific folders
17 | vendor
18 | .env
19 | .env.example
20 |
21 | # Ignore Vite & TypeScript build outputs
22 | .vite
23 | tsconfig.tsbuildinfo
24 |
25 | # Ignore logs & IDE settings
26 | *.log
27 | .DS_Store
28 | Thumbs.db
29 | .vscode
30 | .idea
31 |
32 | # Ignore Prettier cache files
33 | .prettiercache
34 |
35 | # Ignore compiled Blade templates
36 | storage/framework/views/*.php
37 |
38 | # Ignore Blade cache
39 | bootstrap/cache/*.php
40 |
41 | # Ignore GitHub Actions workflow files
42 | .github/**/*.yml
43 |
44 | # Ignore formatted files
45 | *.min.js
46 | *.min.css
47 |
--------------------------------------------------------------------------------
/database/migrations/2025_08_01_183554_create_newsletter_subscribers_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('email')->unique();
17 | $table->boolean('is_active')->default(true);
18 | $table->timestamps();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::dropIfExists('newsletter_subscribers');
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/resources/js/components/single-select.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Select from 'react-select'
3 | import { SelectOption } from '@/types'
4 |
5 | interface SingleSelectProps {
6 | options: SelectOption[]
7 | value: SelectOption | null
8 | onChange: (selectedOption: SelectOption | null) => void
9 | placeholder?: string
10 | }
11 |
12 | const SingleSelect: React.FC = ({
13 | options,
14 | value,
15 | onChange,
16 | placeholder = 'Select an option',
17 | }) => {
18 | return (
19 |
27 | )
28 | }
29 |
30 | export default SingleSelect
31 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Application/Feed/ToggleCommentVoteController.php:
--------------------------------------------------------------------------------
1 | execute($feedComment, $request->user(), $request->vote_type);
16 |
17 | return response()->json([
18 | 'upvotes' => $result['upvotes_count'],
19 | 'downvotes' => $result['downvotes_count'],
20 | 'action' => $result['action'],
21 | ]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vendor/feed/style.css:
--------------------------------------------------------------------------------
1 | .layout-content {
2 | max-width: 640px;
3 | margin-left: auto;
4 | margin-right: auto;
5 |
6 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
7 | }
8 |
9 | hr {
10 | margin: 2rem 0;
11 | }
12 |
13 | .post {
14 | margin-bottom: 2rem;
15 | }
16 |
17 | .post .title {
18 | font-size: 1.5rem;
19 | line-height: 2rem;
20 | margin-bottom: 1rem;
21 | }
22 |
23 | .post .summary {
24 | font-size: 1rem;
25 | line-height: 1.5rem;
26 | margin-bottom: 1rem;
27 | }
28 |
29 | .post .published-info {
30 | font-size: 0.75rem;
31 | line-height: 1rem;
32 | color: #666;
33 | }
34 |
35 | img {
36 | max-width: 100%;
37 | }
38 |
--------------------------------------------------------------------------------
/resources/js/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const labelVariants = cva(
8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_08_231823_create_index_package_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignIdFor(Index::class);
19 | $table->foreignIdFor(Package::class);
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::dropIfExists('index_package');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/app/Filament/Widgets/StatsOverview.php:
--------------------------------------------------------------------------------
1 | description('All registered users')
17 | ->color('primary'),
18 | BaseWidget\Stat::make('Blog Posts', BlogPost::count())
19 | ->description('Blog Posts')
20 | ->color('success'),
21 | BaseWidget\Stat::make('Total Packages', Package::count())
22 | ->description('Available packages')
23 | ->color('warning'),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/PasswordController.php:
--------------------------------------------------------------------------------
1 | validate([
19 | 'current_password' => ['required', 'current_password'],
20 | 'password' => ['required', Password::defaults(), 'confirmed'],
21 | ]);
22 |
23 | $request->user()->update([
24 | 'password' => Hash::make($validated['password']),
25 | ]);
26 |
27 | return back();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Resources/UserResource.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | public function toArray(Request $request): array
18 | {
19 | return [
20 | 'name' => $this->name,
21 | 'email' => $this->email,
22 | 'username' => $this->username,
23 | 'avatar' => $this->avatar,
24 | 'packages' => PackageResource::collection($this->whenLoaded('packages')),
25 | 'packages_count' => $this->whenCounted('packages'),
26 | 'email_verified_at' => $this->email_verified_at,
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/resources/js/Layouts/GuestLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@inertiajs/react'
2 | import { PropsWithChildren } from 'react'
3 | import Image from '@/components/image'
4 |
5 | export default function Guest({ children }: PropsWithChildren) {
6 | return (
7 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/resources/js/components/danger-button.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonHTMLAttributes } from 'react'
2 |
3 | export default function DangerButton({
4 | className = '',
5 | disabled,
6 | children,
7 | ...props
8 | }: ButtonHTMLAttributes) {
9 | return (
10 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/database/migrations/2025_02_12_191127_create_admins_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('name');
17 | $table->string('email')->unique();
18 | $table->string('password');
19 | $table->rememberToken();
20 | $table->timestamps();
21 | $table->softDeletes();
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | */
28 | public function down(): void
29 | {
30 | Schema::dropIfExists('admins');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/ProfileUpdateRequest.php:
--------------------------------------------------------------------------------
1 | |string>
15 | */
16 | public function rules(): array
17 | {
18 | return [
19 | 'name' => ['required', 'string', 'max:255'],
20 | 'email' => [
21 | 'required',
22 | 'string',
23 | 'lowercase',
24 | 'email',
25 | 'max:255',
26 | Rule::unique(Admin::class)->ignore($this->admin()->id),
27 | ],
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/database/migrations/2025_07_20_153252_add_user_id_to_packages_table.php:
--------------------------------------------------------------------------------
1 | foreignIdFor(\App\Models\User::class)
16 | ->after('index_id')
17 | ->nullable()
18 | ->constrained();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::table('packages', function (Blueprint $table) {
28 | $table->dropForeign('packages_user_id_foreign');
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2025_08_01_231351_alter_user_agent_length_in_blog_post_views_table.php:
--------------------------------------------------------------------------------
1 | text('user_agent')
16 | ->nullable()
17 | ->change();
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | */
24 | public function down(): void
25 | {
26 | Schema::table('blog_post_views', function (Blueprint $table) {
27 | $table->string('user_agent')
28 | ->nullable()
29 | ->change();
30 | });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/Settings/SeoSettings.php:
--------------------------------------------------------------------------------
1 | validate([
20 | 'current_password' => ['required', new AdminCurrentPassword],
21 | 'password' => ['required', Password::defaults(), 'confirmed'],
22 | ]);
23 |
24 | $request->admin()->update([
25 | 'password' => Hash::make($validated['password']),
26 | ]);
27 |
28 | return back();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_09_021616_make_index_id_nullable_in_packages_table.php:
--------------------------------------------------------------------------------
1 | foreignIdFor(Index::class)
17 | ->nullable()
18 | ->change();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | */
25 | public function down(): void
26 | {
27 | Schema::table('packages', function (Blueprint $table) {
28 | $table->foreignIdFor(Index::class)
29 | ->nullable(false)
30 | ->change();
31 | });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/VerifyEmailController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
18 | return redirect()->intended(route('app.feed.home', absolute: false).'?verified=1');
19 | }
20 |
21 | if ($request->user()->markEmailAsVerified()) {
22 | event(new Verified($request->user()));
23 | }
24 |
25 | return redirect()->intended(route('app.feed.home', absolute: false).'?verified=1');
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2025_02_18_032836_create_category_package_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->foreignIdFor(Package::class)->constrained()->cascadeOnDelete();
19 | $table->foreignIdFor(Category::class)->constrained()->cascadeOnDelete();
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::dropIfExists('category_package');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2025_12_08_222333_create_feed_post_bookmarks_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->foreignId('feed_post_id')->constrained()->cascadeOnDelete();
17 | $table->foreignId('user_id')->constrained()->cascadeOnDelete();
18 | $table->timestamps();
19 |
20 | $table->unique(['feed_post_id', 'user_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::dropIfExists('feed_post_bookmarks');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_15_222014_create_blog_post_category_table.php:
--------------------------------------------------------------------------------
1 | foreignIdFor(Category::class)
18 | ->constrained()
19 | ->cascadeOnDelete();
20 | $table->foreignIdFor(BlogPost::class)
21 | ->constrained()
22 | ->cascadeOnDelete();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('blog_post_category');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/resources/js/components/multi-select.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Select from 'react-select'
3 |
4 | interface Option {
5 | value: string | number
6 | label: string
7 | }
8 |
9 | interface MultiSelectProps {
10 | options: Option[]
11 | value: Option[]
12 | onChange: (selectedOptions: Option[]) => void
13 | placeholder?: string
14 | }
15 |
16 | const MultiSelect: React.FC = ({
17 | options,
18 | value,
19 | onChange,
20 | placeholder = 'Select multiple options',
21 | }) => {
22 | return (
23 |