+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/resources/js/components/ui/tabs.tsx b/resources/js/components/ui/tabs.tsx
new file mode 100644
index 0000000..85d83be
--- /dev/null
+++ b/resources/js/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx
index 0f198a5..53c4aee 100644
--- a/resources/js/components/user-menu-content.tsx
+++ b/resources/js/components/user-menu-content.tsx
@@ -1,11 +1,10 @@
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { UserInfo } from '@/components/user-info';
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
-import { logout } from '@/routes';
import { edit } from '@/routes/profile';
import { type User } from '@/types';
-import { Link, router } from '@inertiajs/react';
-import { LogOut, Settings } from 'lucide-react';
+import { Link } from '@inertiajs/react';
+import { Settings } from 'lucide-react';
interface UserMenuContentProps {
user: User;
@@ -14,11 +13,6 @@ interface UserMenuContentProps {
export function UserMenuContent({ user }: UserMenuContentProps) {
const cleanup = useMobileNavigation();
- const handleLogout = () => {
- cleanup();
- router.flushAll();
- };
-
return (
<>
@@ -35,13 +29,13 @@ export function UserMenuContent({ user }: UserMenuContentProps) {
-
-
-
-
- Log out
-
-
+ {/**/}
+ {/**/}
+ {/* */}
+ {/* */}
+ {/* Log out*/}
+ {/* */}
+ {/**/}
>
);
}
diff --git a/resources/js/docs/introduction.mdx b/resources/js/docs/introduction.mdx
new file mode 100644
index 0000000..1972ed8
--- /dev/null
+++ b/resources/js/docs/introduction.mdx
@@ -0,0 +1,56 @@
+---
+title: Introduction to Astrify UI
+description: Astrify UI is a collection of full-stack modules for React and Laravel applications.
+date: 2025-09-30
+label: Introduction
+tags: []
+category: Getting Started
+---
+
+Astrify UI is a collection of full-stack modules for React and Laravel applications. While traditional component libraries give you front-end pieces that you need to wire up yourself, Astrify UI provides complete modules and usage examples that handle both client and server-side functionality.
+
+## Prerequisites
+
+Astrify UI modules and usage examples are tested and guaranteed to install seamlessly into applications built with the [Laravel React Starter Kit](https://github.com/laravel/react-starter-kit). If you're installing into an existing application, please ensure you have the following prerequisites configured for your laravel application:
+
+- **React**
+- **Inertia.js** for seamless client-server communication
+- **Tailwind CSS** for styling
+- **Shadcn/ui** components installed
+
+## Why Astrify UI?
+
+### 1. More Than Components
+
+Every developer who runs `laravel new` and installs the React starter kit faces the same challenges. While there are countless component libraries available, they typically only solve the front-end piece of the puzzle.
+
+Astrify UI is different. Our modules are **full-stack solutions** that include:
+
+- Client-side UI components built with React
+- Server-side Laravel controllers and logic
+- Detailed usage examples that marry the two
+
+### 2. Own Your Code
+
+Unlike traditional packages that live in `vendor` directories, Astrify UI uses the [Shadcn CLI](https://ui-private.shadcn.com/docs/cli) to install modules directly into your application's codebase. This approach means:
+
+- **No vendor lock-in**: The code becomes yours to modify and extend
+- **Full customization**: Change anything to match your specific needs
+- **Less dependencies**: Fewer external package to maintain or worry about updates
+- **AI-friendly**: Your AI coding assistant has full context and can easily modify the code to fit your requirements
+
+### 3. Built on Shadcn/UI
+
+All Astrify UI modules are built using **Shadcn/UI components**, which means they fit perfectly with Laravel React applications already using Shadcn and work seamlessly with any Shadcn theme you apply.
+
+## Why Make This?
+
+The Laravel ecosystem has excellent component libraries for Livewire, such as [Flux](https://fluxui.dev/) and [Filament](https://filamentphp.com). We believe the React side of Laravel deserves the same level of polish and convenience. Astrify UI bridges this gap by providing React developers with the same rapid development experience that Livewire developers have enjoyed.
+
+## Getting Started
+
+Ready to supercharge your Laravel React application? Head over to the [File Upload](./upload) documentation to get started with your first Astrify UI module.
+
+---
+
+_Astrify UI: Full-stack modules for Laravel and React. Install once, customize forever._
diff --git a/resources/js/docs/json-table.mdx b/resources/js/docs/json-table.mdx
new file mode 100644
index 0000000..8904b1e
--- /dev/null
+++ b/resources/js/docs/json-table.mdx
@@ -0,0 +1,116 @@
+---
+title: Json Table
+description: A complete paginated table solution integrated with Laravel Inertia.js, featuring numeric and simple pagination styles with seamless backend integration using JSON data sources.
+date: 2025-09-30
+label: JSON Table
+tags: [table, pagination, inertia, laravel, component]
+category: Modules
+---
+
+import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { Table, ListOrdered, Database, Zap } from 'lucide-react';
+import { TablePreview, JsonTablePreviewSource } from '@/components/previews/table-preview';
+
+This module wraps the [Paginated Table](./paginated-table) module with a component that fetches data from the given URL and expects the response to be in the same format as Laravel's paginator json output.
+
+} code={JsonTablePreviewSource} title="json-table-example.tsx" language="tsx" />
+
+## Installation
+
+```bash
+npx shadcn@latest add https://astrify.com/r/json-table.json
+```
+
+## Usage
+
+This usage example loads data via JSON fetch requests. To load data via Inertia requests see the [Paginated Table Module](./paginated-table).
+
+
+{`import { JsonTable } from '@/components/astrify/json-table';
+ import AppLayout from '@/layouts/app-layout';
+ import type { BreadcrumbItem } from '@/types';
+ import { Head } from '@inertiajs/react';
+
+ const breadcrumbs: BreadcrumbItem[] = [
+ {
+ title: 'JSON Table Example',
+ href: '/json-table-example',
+ },
+ ];
+
+ export default function JsonTableExample() {
+ return (
+
+
+
+
+
JSON Table Example
+
+ This table uses JSON requests for it's data instead of Inertia requests. Useful when you need multiple tables on a single page
+ or don't want to tie the pagination to the rest of the page.
+
+
+
+ {
+ if (key === 'created_at') return new Date(value).toLocaleDateString();
+ return value;
+ }}
+ />
+
+
+ );
+ }
+
+`}
+
+
+
+{`Route::get('/json-table-data-example', function () {
+ $users = \\App\\Models\\User::select('id', 'name', 'email', 'created_at')
+ ->orderBy('id', 'desc')
+ ->paginate(10);
+
+ return response()->json($users);
+ });
+
+ Route::get('/json-table-example', function () {
+ return Inertia::render('json-table-example');
+ });`}
+
+
+Optionally seed your database with test data to see the pagination in action:
+```php
+artisan tinker
+\App\Models\User::factory(100)->create();
+```
+
+## Props
+
+### JsonTable
+
+| Prop | Type | Description |
+| ----------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
+| `url` | `string` | The endpoint URL to fetch paginated data from |
+| `columns` | `JsonTableColumn[]` | Array of column definitions with `key` and `label` properties |
+| `paginationType` | `"simple" \| "numeric"` (optional) | The pagination style to use (defaults to `"numeric"`) |
+| `formatCell` | `(key: string, value: any) => string \| number` (optional) | Optional function to format cell values before display |
+
+### JsonTableColumn
+
+| Prop | Type | Description |
+| ------- | -------- | ------------------------------------------ |
+| `key` | `string` | The key in the data object to display |
+| `label` | `string` | The column header label |
diff --git a/resources/js/docs/paginated-table.mdx b/resources/js/docs/paginated-table.mdx
new file mode 100644
index 0000000..9e39e40
--- /dev/null
+++ b/resources/js/docs/paginated-table.mdx
@@ -0,0 +1,149 @@
+---
+title: Paginated Table
+description: A complete paginated table solution integrated with Laravel Inertia.js, featuring numeric and simple pagination styles with seamless backend integration.
+date: 2025-09-30
+label: Paginated Table
+tags: [table, pagination, inertia, laravel, component]
+category: Modules
+---
+
+import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { Table, ListOrdered, Database, Zap } from 'lucide-react';
+import { TablePreview, PaginatedTablePreviewSource } from '@/components/previews/table-preview';
+
+This module wraps the [Shadcn/UI Table](https://ui.shadcn.com/docs/components/table) components with Laravel's default paginator. The provided integration example uses inertia as the data source.
+
+} code={PaginatedTablePreviewSource} title="paginated-table-example.tsx" language="tsx" />
+
+## Installation
+
+```bash
+npx shadcn@latest add https://astrify.com/r/paginated-table.json
+```
+
+## Usage
+
+This usage example loads data via Inertia requests. As you paginate this table the page number would be reflected in the address bar. To load data via JSON fetch requests see the [Json Table Module](./json-table).
+
+
+{`import { PaginatedTable } from '@/components/astrify/paginated-table';
+ import AppLayout from '@/layouts/app-layout';
+ import type { BreadcrumbItem } from '@/types';
+ import { Head, router } from '@inertiajs/react';
+
+ const breadcrumbs: BreadcrumbItem[] = [
+ {
+ title: 'Inertia Table Example',
+ href: '/inertia-table-example',
+ },
+ ];
+
+ interface User {
+ id: number;
+ name: string;
+ email: string;
+ created_at: string;
+ }
+
+ interface Props {
+ users: {
+ data: User[];
+ current_page: number;
+ last_page: number;
+ total: number;
+ links: Array<{
+ url: string | null;
+ label: string;
+ active: boolean;
+ }>;
+ };
+ }
+
+ export default function InertiaTableExample({ users }: Props) {
+ const handlePageChange = (url: string | null) => {
+ if (!url) return;
+ router.get(url, {}, { preserveScroll: true, preserveState: true });
+ };
+
+ return (
+
+
+
+
+
Inertia Table
+
+ This table loads it's data via Inertia requests. As you paginate this table the page number is reflected in the address bar.
+
+
+ );
+ }
+`}
+
+
+
+{`Route::get('inertia-table-example', function () {
+ $users = \\App\\Models\\User::select('id', 'name', 'email', 'created_at')
+ ->orderBy('id', 'desc')
+ ->paginate(10)
+ ->onEachSide(1);
+
+ return Inertia::render('inertia-table-example', [
+ 'users' => $users,
+ ]);
+})->name('table.index');`}
+
+
+
+Optionally seed your database with test data to see the pagination in action:
+```php
+artisan tinker
+\App\Models\User::factory(100)->create();
+```
+
+## Props
+
+### PaginatedTable
+
+| Prop | Type | Description |
+| ------------- | ---------------------------- | ------------------------------------------------------------------------- |
+| `columns` | `string[]` | Array of column header labels |
+| `data` | `(string \| number)[][]` | 2D array of table data rows |
+| `pagination` | `PaginatorConfig` | Pagination configuration object |
+| `placeholder` | `React.ReactNode` (optional) | Custom content to show when data is empty (loading, errors, empty states) |
+
+### PaginatorConfig
+
+| Prop | Type | Description |
+| -------------- | ------------------------------- | --------------------------------------------- |
+| `type` | `"simple" \| "numeric"` | The pagination style to use |
+| `currentPage` | `number` | Current page number |
+| `lastPage` | `number` | Total number of pages |
+| `links` | `PaginationLinkType[]` | Array of pagination link objects from Laravel |
+| `onChangePage` | `(url: string \| null) => void` | Callback function when page changes |
+| `total` | `number` (optional) | Total number of records |
+
+### PaginationLinkType
+
+| Prop | Type | Description |
+| -------- | ----------------- | ------------------------------- |
+| `url` | `string \| null` | URL for the pagination link |
+| `label` | `string` | Label text for the link |
+| `active` | `boolean` | Whether this link is active |
diff --git a/resources/js/docs/upload.mdx b/resources/js/docs/upload.mdx
new file mode 100644
index 0000000..805c4fa
--- /dev/null
+++ b/resources/js/docs/upload.mdx
@@ -0,0 +1,287 @@
+---
+title: File Upload Module
+description: A complete file upload solution integrated with Laravel Inertia.js, featuring drag & drop, S3 storage, and comprehensive validation.
+date: 2025-09-29
+label: File Upload
+tags: [upload, inertia, s3, laravel, component]
+category: Modules
+---
+
+import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import { Upload, FileText, Cloud, Shield, Zap } from 'lucide-react';
+import { UploadPreview, UploadPreviewSource, ImageUploadDemo, ImageUploadDemoSource } from '@/components/previews/upload-preview';
+
+## Multi-file Upload
+
+Upload multiple files with drag & drop support, progress tracking, and error handling.
+
+} code={UploadPreviewSource} title="upload-demo.tsx" language="tsx" />
+
+## Single Image Upload
+
+Upload a single image file with preview and validation.
+
+} code={ImageUploadDemoSource} title="image-upload-demo.tsx" language="tsx" />
+
+## Installation
+
+### 1: Install the Module
+
+You can install the upload module using the shadcn registry system:
+
+```bash
+npx shadcn@latest add https://astrify.com/r/upload.json
+```
+
+This command will install the necessary JavaScript dependencies, shadcn/ui components, and add the SignedUrlController.php to your application.
+
+### 2: Configure Routes
+
+Reference the SignedUrlController in your routes file
+
+```php
+Route::post('/upload/signed-url',
+ \App\Http\Controllers\Astrify\SignedUrlController::class
+)
+// if the environment is public be sure to require auth
+//->middleware(['auth'])
+->name('upload.signed-url');
+```
+
+### 3: Install Laravel Dependencies
+
+Install the Flysystem S3 package via the Composer package manager:
+
+```bash
+composer require league/flysystem-aws-s3-v3:^3.0
+```
+
+### 4: Configure your S3 Storage
+
+For local development we recommend using [MinIO](https://min.io/) for locally-hosted S3-compatible storage. There are many ways to run MinIO on your machine but the remaining steps will assume you ran it in Docker with the following command:
+
+```bash
+docker run \
+ -p 9000:9000 -p 9001:9001 \
+ -v "$HOME/minio-data:/data" \
+ quay.io/minio/minio server /data --console-address ":9001"
+```
+
+Next login to the MinIO console at [http://localhost:9001](http://localhost:9001) using the default credentials (`minioadmin` / `minioadmin`) and create a new bucket called `my-bucket`.
+
+Finally set the following environment variables in your `.env` file:
+
+```env
+AWS_BUCKET=my-bucket
+AWS_ACCESS_KEY_ID=minioadmin
+AWS_USE_PATH_STYLE_ENDPOINT=true
+AWS_SECRET_ACCESS_KEY=minioadmin
+AWS_DEFAULT_REGION=us-east-1
+AWS_URL=http://127.0.0.1:9000/my-bucket
+AWS_ENDPOINT=http://127.0.0.1:9000
+```
+
+
+
+ Pro Tip
+
+ If you're using a real Amazon S3 bucket don't forget to configure the bucket's CORS policy to allow POST requests from your domain.
+
+
+
+## Usage
+
+This usage example provides a full page Inertia component. It integrates the upload module into an Inertia form submission. It demonstrates how to pass references for the successfully uploaded files to the Inertia useForm hook and how to prevent form submission until all file uploads are completed. The route examples shows how to validate and receive the files in your Laravel route or controller.
+
+
+{`import { Dropzone, Errors, Header, List } from '@/components/astrify/upload';
+ import { Button } from '@/components/ui/button';
+ import { Input } from '@/components/ui/input';
+ import { Label } from '@/components/ui/label';
+ import AppLayout from '@/layouts/app-layout';
+ import { type BreadcrumbItem } from '@/types';
+ import { FileUploadProvider, useFileUpload } from '@astrify/react-s3-upload';
+ import { Head, useForm } from '@inertiajs/react';
+ import { LoaderCircle } from 'lucide-react';
+ import type { FormEventHandler } from 'react';
+ import { toast } from 'sonner';
+
+ const breadcrumbs: BreadcrumbItem[] = [
+ {
+ title: 'Upload Example',
+ href: '/upload',
+ },
+ ];
+
+ export default function InertiaUploadExample() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ type UploadForm = {
+ name: string;
+ uploadedFiles: Array<{
+ id: string;
+ name: string;
+ sha256: string;
+ size: number;
+ type: string;
+ }>;
+ };
+
+ function FormContent({ submitEndpoint = '/upload' }: { submitEndpoint: string }) {
+ const { files, hasComplete, hasPending, hasUploading, hasErrors, removeAll } = useFileUpload();
+
+ const { data, setData, post, processing, errors, reset, transform } = useForm({
+ name: '',
+ uploadedFiles: [],
+ });
+
+ const submitForm: FormEventHandler = (e) => {
+ e.preventDefault();
+
+ // Extract only completed files for submission
+ const completedFiles = files.filter((f) => f.status === 'complete');
+
+ // Prepare the uploaded files data
+ const uploadedFiles = completedFiles.map((f) => ({
+ id: f.id,
+ name: f.name,
+ sha256: f.sha256,
+ size: f.size,
+ type: f.type,
+ }));
+
+ // Transform the form data to include uploaded files and submit
+ transform((formData) => ({
+ ...formData,
+ uploadedFiles,
+ }));
+
+ // Submit using Inertia
+ post(submitEndpoint, {
+ onFinish: () => {
+ // Reset the name field after successful submission
+ reset('name');
+ removeAll(); // Clear all files from the upload context
+ toast.success('Form submitted successfully', {
+ richColors: true,
+ });
+ },
+ });
+ };
+
+ // Enable submit only when all uploads are complete and form has a name
+ const canSubmit = hasComplete && !hasPending && !hasUploading && !hasErrors && data.name.length > 0;
+
+ return (
+
+ );
+ }
+`}
+
+
+
+{`Route::get('/upload', function () {
+ return Inertia::render('inertia-upload-example');
+})->name('upload.index');
+
+Route::post('/upload', function (\\Illuminate\\Http\\Request $request) {
+ // Validate the incoming request
+ $validated = $request->validate([
+ 'name' => 'required|string|max:255',
+ 'uploadedFiles' => 'required|array|min:1',
+ 'uploadedFiles.*.id' => 'required|string',
+ 'uploadedFiles.*.name' => 'required|string',
+ 'uploadedFiles.*.sha256' => 'required|string',
+ 'uploadedFiles.*.size' => 'required|integer|min:0',
+ 'uploadedFiles.*.type' => 'required|string',
+ ]);
+
+ \\Illuminate\\Support\\Facades\\Log::info('Document creation request received', [
+ 'name' => $validated['name'],
+ 'uploaded_files' => $validated['uploadedFiles'],
+ ]);
+
+ return back()->with('success', 'Files uploaded successfully');
+})->name('upload.store');`}
+
+
+## Security Notes
+
+This module is designed to ensure secure file uploads to your S3 bucket. Here’s an overview of the process:
+
+1. **Private bucket configuration:**
+ Your S3 bucket should be configured as **private**. This prevents users from uploading or accessing files directly and ensures all access goes through temporary signed URLs issued by your Laravel application.
+
+2. **Requesting a signed URL:**
+ The client requests a signed URL from the `SignedUrlController`, providing metadata about the file(s) to be uploaded: name, size, type, and SHA256 hash.
+
+3. **Server-side validation:**
+ Before issuing a signed URL, the server validates the provided metadata. If any check fails, the request is denied.
+ - File size (must not exceed configured limits)
+ - File type (must match allowed MIME types)
+ - SHA256 hash (used for file integrity verification)
+
+4. **URL generation:**
+ If validation passes, the server returns a signed URL containing the verified metadata.
+
+5. **Upload enforcement:**
+ If the client attempts to upload a file that does not match the metadata encoded in the signed URL, the S3 storage will reject the upload.
+
+6. **Tamper protection:**
+ If the client modifies any claims in the signed URL, the signature becomes invalid and the S3 storage will reject the upload.
+
+> **Next Steps:** Try implementing the upload component in your application and customize it to match your specific requirements.
diff --git a/resources/js/layouts/docs-layout.tsx b/resources/js/layouts/docs-layout.tsx
new file mode 100644
index 0000000..08f0838
--- /dev/null
+++ b/resources/js/layouts/docs-layout.tsx
@@ -0,0 +1,139 @@
+import { AppContent } from '@/components/app-content';
+import { AppShell } from '@/components/app-shell';
+import { AppSidebar } from '@/components/app-sidebar';
+import { AppSidebarHeader } from '@/components/app-sidebar-header';
+import { DocsTableOfContents } from '@/components/docs/docs-toc';
+import { show as docsRoute } from '@/routes/docs';
+import { type DocsManifest, type DocsTableOfContentsItem } from '@/types';
+import React from 'react';
+
+type Props = {
+ slug: string;
+ meta?: Record;
+ toc?: DocsTableOfContentsItem[] | null;
+ manifest?: DocsManifest;
+ children: React.ReactNode;
+};
+
+export default function DocsLayout({ slug, meta = {}, toc, manifest, children }: Props) {
+ const title = (meta.title as string) ?? slug.split('/').pop();
+ const label = (meta.label as string) ?? '';
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/resources/js/pages/astrify-examples/inertia-upload-example.tsx b/resources/js/pages/astrify-examples/inertia-upload-example.tsx
new file mode 100644
index 0000000..6d03c50
--- /dev/null
+++ b/resources/js/pages/astrify-examples/inertia-upload-example.tsx
@@ -0,0 +1,132 @@
+import { Dropzone, Errors, Header, List } from '@/components/astrify/upload';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import AppLayout from '@/layouts/app-layout';
+import { type BreadcrumbItem } from '@/types';
+import { FileUploadProvider, useFileUpload } from '@astrify/react-s3-upload';
+import { Head, useForm } from '@inertiajs/react';
+import { LoaderCircle } from 'lucide-react';
+import type { FormEventHandler } from 'react';
+import { toast } from 'sonner';
+
+const breadcrumbs: BreadcrumbItem[] = [
+ {
+ title: 'Upload Example',
+ href: '/upload',
+ },
+];
+
+export default function InertiaUploadExample() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+type UploadForm = {
+ name: string;
+ uploadedFiles: Array<{
+ id: string;
+ name: string;
+ sha256: string;
+ size: number;
+ type: string;
+ }>;
+};
+
+function FormContent({ submitEndpoint = '/upload' }: { submitEndpoint: string }) {
+ const { files, hasComplete, hasPending, hasUploading, hasErrors, removeAll } = useFileUpload();
+
+ const { data, setData, post, processing, errors, reset, transform } = useForm({
+ name: '',
+ uploadedFiles: [],
+ });
+
+ const submitForm: FormEventHandler = (e) => {
+ e.preventDefault();
+
+ // Extract only completed files for submission
+ const completedFiles = files.filter((f) => f.status === 'complete');
+
+ // Prepare the uploaded files data
+ const uploadedFiles = completedFiles.map((f) => ({
+ id: f.id,
+ name: f.name,
+ sha256: f.sha256,
+ size: f.size,
+ type: f.type,
+ }));
+
+ // Transform the form data to include uploaded files and submit
+ transform((formData) => ({
+ ...formData,
+ uploadedFiles,
+ }));
+
+ // Submit using Inertia
+ post(submitEndpoint, {
+ onFinish: () => {
+ // Reset the name field after successful submission
+ reset('name');
+ removeAll(); // Clear all files from the upload context
+ toast.success('Form submitted successfully', {
+ richColors: true,
+ });
+ },
+ });
+ };
+
+ // Enable submit only when all uploads are complete and form has a name
+ const canSubmit = hasComplete && !hasPending && !hasUploading && !hasErrors && data.name.length > 0;
+
+ return (
+
+ );
+}
diff --git a/resources/js/pages/astrify-examples/json-table-example.tsx b/resources/js/pages/astrify-examples/json-table-example.tsx
new file mode 100644
index 0000000..4cdfdaa
--- /dev/null
+++ b/resources/js/pages/astrify-examples/json-table-example.tsx
@@ -0,0 +1,43 @@
+import { JsonTable } from '@/components/astrify/json-table';
+import AppLayout from '@/layouts/app-layout';
+import type { BreadcrumbItem } from '@/types';
+import { Head } from '@inertiajs/react';
+
+const breadcrumbs: BreadcrumbItem[] = [
+ {
+ title: 'JSON Table Example',
+ href: '/json-table-example',
+ },
+];
+
+export default function JsonTableExample() {
+ return (
+
+
+
+
+
JSON Table Example
+
+ This table uses JSON requests for it's data instead of Inertia requests. Useful when you need multiple tables on a single page
+ or don't want to tie the pagination to the rest of the page.
+
+
+
+ {
+ if (key === 'created_at') return new Date(value).toLocaleDateString();
+ return value;
+ }}
+ />
+
+
+ );
+}
diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx
index 288f976..1b2b3f9 100644
--- a/resources/js/pages/dashboard.tsx
+++ b/resources/js/pages/dashboard.tsx
@@ -1,3 +1,4 @@
+import { Upload } from '@/components/astrify/upload';
import { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
import AppLayout from '@/layouts/app-layout';
import { dashboard } from '@/routes';
@@ -28,7 +29,17 @@ export default function Dashboard() {
-
+
diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx
index 452fb2d..6c113cd 100644
--- a/resources/js/pages/welcome.tsx
+++ b/resources/js/pages/welcome.tsx
@@ -1,792 +1,21 @@
-import { dashboard, login, register } from '@/routes';
-import { type SharedData } from '@/types';
-import { Head, Link, usePage } from '@inertiajs/react';
+import { HeroSection } from '@/components/landing/hero-section';
+import { ModulesSection } from '@/components/landing/modules-section';
+import { TemplateSection } from '@/components/landing/template-section';
+import HomeLayout from '@/layouts/home-layout';
+import { Head } from '@inertiajs/react';
export default function Welcome() {
- const { auth } = usePage().props;
-
return (
- <>
-
-
-
+
+
+
-
-
-
-
-
-
-
-
Let's get started
-
- Laravel has an incredibly rich ecosystem.
-
- We suggest starting with the following.
-