Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(npm run lint)"
],
"deny": [],
"ask": []
}
}
135 changes: 135 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# This workflow builds and publishes the shadcn registry to GitHub Pages when tags are pushed

name: Publish Registry

on:
push:
tags:
- '*'

permissions:
contents: read
pages: write
id-token: write

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build Registry
run: |
# Build registry components
pnpm registry:build

# Create output directory for registry
mkdir -p registry-static

# Copy registry files to output
cp -r public/r registry-static/r

# Add .nojekyll to serve files starting with underscore
touch registry-static/.nojekyll

# Create a simple registry index page with links to all components
cat > registry-static/r/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Astrify UI - shadcn Registry</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; line-height: 1.6; }
h1 { color: #333; margin-bottom: 0.5rem; }
h2 { color: #555; margin-top: 2rem; }
.subtitle { color: #666; margin-bottom: 2rem; }
.component-list { list-style: none; padding: 0; }
.component-list li { margin: 1rem 0; padding: 1rem; background: #f9f9f9; border-radius: 6px; }
.component-list a { color: #0066cc; text-decoration: none; font-weight: 500; font-size: 1.1rem; }
.component-list a:hover { text-decoration: underline; }
.component-desc { color: #666; margin-top: 0.25rem; font-size: 0.9rem; }
pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto; }
.info { background: #e3f2fd; padding: 1rem; border-radius: 6px; margin: 1rem 0; border-left: 4px solid #2196f3; }
</style>
</head>
<body>
<h1>Astrify UI - shadcn Registry</h1>
<p class="subtitle">Full-stack modules for Laravel, Inertia, and React. Production-ready components with pre-wired backends.</p>

<div class="info">
<strong>Registry URL:</strong> <code>https://astrify.com/r/</code><br>
<strong>Docs:</strong> <a href="https://astrify.com">astrify.com</a>
</div>

<h2>Available Modules:</h2>
<ul class="component-list">
<li>
<a href="upload.json">upload.json</a>
<div class="component-desc">Complete file upload module with S3 storage, signed URLs, and Laravel backend</div>
</li>
<li>
<a href="paginated-table.json">paginated-table.json</a>
<div class="component-desc">Table component with built-in pagination for numeric and simple pagination styles</div>
</li>
<li>
<a href="json-table.json">json-table.json</a>
<div class="component-desc">Table component that fetches and displays paginated JSON data from API endpoints</div>
</li>
</ul>

<h2>Installation:</h2>
<p>Install any module using the shadcn CLI:</p>
<pre><code>npx shadcn@latest add https://astrify.com/r/upload.json</code></pre>

<p>Or use the GitHub Pages URL directly:</p>
<pre><code>npx shadcn@latest add https://astrify.github.io/ui/r/paginated-table.json</code></pre>

<h2>Documentation:</h2>
<p>For detailed installation instructions, usage examples, and API reference, visit <a href="https://astrify.com">astrify.com</a>.</p>
</body>
</html>
EOF

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./registry-static

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/public/build
/public/hot
/public/storage
/public/r
/resources/js/actions
/resources/js/routes
/resources/js/wayfinder
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
resources/js/components/ui/*
resources/js/components/upload/*
resources/js/pages/docs/*
resources/js/posts/*
resources/views/mail/*
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Astrify UI

**Full-stack modules for Laravel and React.**

Astrify UI provides production-ready modules that combine modern shadcn/ui interfaces with pre-wired Laravel backends. Install via the shadcn CLI for instant integration—no dependencies to manage, just code you own.

## What is Astrify UI?

While traditional component libraries give you front-end pieces to wire up yourself, Astrify UI provides **complete full-stack modules**. Each module includes:

- React UI components built with shadcn/ui
- Laravel controllers, routes, and backend logic
- Detailed usage examples that connect everything together

The code is installed directly into your project using the shadcn CLI, giving you 100% ownership and control.

## Available Modules

- **[File Upload with S3](https://astrify.com/docs/upload)** - Signed URLs, storage management, and image previews
- **[Paginated Tables](https://astrify.com/docs/paginated-table)** - Server-side pagination with Laravel collections
- **[JSON Tables](https://astrify.com/docs/json-table)** - Fetch and display paginated JSON data from API endpoints

More modules released weekly.

## Documentation

For installation instructions and usage guides, visit [https://astrify-ui.test/docs/introduction](https://astrify-ui.test/docs/introduction)
105 changes: 105 additions & 0 deletions app/Http/Controllers/Astrify/SignedUrlController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace App\Http\Controllers\Astrify;

use Aws\S3\S3Client;
use Aws\Signature\S3SignatureV4;
use Aws\Signature\SignatureProvider;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class SignedUrlController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(Request $request): JsonResponse
{
// Authorize the upload
//Gate::authorize('uploadFiles', $request->user());

// Validate the array of files
$validated = $request->validate([
'files' => 'required|array|min:1',
'files.*.contentType' => 'required|string',
'files.*.filename' => 'required|string',
'files.*.filesize' => 'required|integer|max:10485760', // 1 MB max
'files.*.sha256' => 'required|string|regex:/^[a-f0-9]{64}$/i',
]);

// Initialize S3 client
$signatureProvider = SignatureProvider::memoize(static function ($version, $service, $region) {
if (($version === 's3v4' || $version === 'v4') && $service === 's3') {
return new MyCustomS3Signature($service, $region);
}

return SignatureProvider::version()($version, $service, $region);
});

$config = [
'region' => config('filesystems.disks.s3.region'),
'version' => 'latest',
'credentials' => [
'key' => config('filesystems.disks.s3.key'),
'secret' => config('filesystems.disks.s3.secret'),
],
'signature_provider' => $signatureProvider,
];

if (config('filesystems.disks.s3.endpoint')) {
$config['endpoint'] = config('filesystems.disks.s3.endpoint');
$config['use_path_style_endpoint'] = config('filesystems.disks.s3.use_path_style_endpoint', false);
}

if (config('filesystems.disks.s3.url')) {
$config['url'] = config('filesystems.disks.s3.url');
}

$client = new S3Client($config);

// Process each file and generate signed URLs
$signedUrls = [];

foreach ($validated['files'] as $fileData) {
// Use the hash as the key (no folder structure, no extension)
$key = 'uploads/'.strtolower($fileData['sha256']);

// Create the presigned request with checksum header
$checksum = base64_encode(hex2bin($fileData['sha256']));

$command = $client->getCommand('PutObject', [
'Bucket' => config('filesystems.disks.s3.bucket'),
'Key' => $key,
'ContentType' => $fileData['contentType'],
'ChecksumSHA256' => $checksum,
'ContentLength' => $fileData['filesize'],
]);

$presignedRequest = $client->createPresignedRequest($command, '+5 minutes');
$uri = $presignedRequest->getUri();

$signedUrls[] = [
'sha256' => $fileData['sha256'], // SHA-256 hash as identifier
'bucket' => config('filesystems.disks.s3.bucket'),
'key' => $key,
'url' => (string) $uri,
'filename' => $fileData['filename'], // Include filename for client reference
];
}

return response()->json(['files' => $signedUrls]);
}
}

// https://github.com/aws/aws-sdk-php/issues/3108
class MyCustomS3Signature extends S3SignatureV4
{
protected function getHeaderBlacklist(): array
{
$deniedList = parent::getHeaderBlacklist();
unset($deniedList['content-length']);

return $deniedList;
}
}
33 changes: 33 additions & 0 deletions app/Http/Controllers/DocsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\File;
use Inertia\Inertia;

class DocsController extends Controller
{
protected function manifest(): array
{
$path = base_path('bootstrap/cache/mdx.json');
if (!File::exists($path)) return [];
return json_decode(File::get($path), true) ?? [];
}

public function show(string $slug)
{
// Slugs in manifest are kebab-case
$manifest = $this->manifest();
$entry = collect($manifest)->firstWhere('slug', $slug);

if (!$entry) abort(404);

// Render the generated page file
return Inertia::render('docs/'.$slug, [
'slug' => $slug,
'meta' => $entry['meta'] ?? [],
'toc' => $entry['toc'] ?? null,
'manifest' => $manifest,
]);
}
}
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
],
"license": "MIT",
"require": {
"php": "^8.2",
"php": "^8.3",
"inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"laravel/wayfinder": "^0.1.9"
"laravel/wayfinder": "^0.1.9",
"league/flysystem-aws-s3-v3": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
Expand Down Expand Up @@ -83,4 +84,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}
Loading