* feat: new components, deploy storybook * refactor: simplify component field definitions and enhance button loading state - Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook. - Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props. - Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback. - Minor CSS adjustments for better layout and styling consistency. * chore: add field, card, password components * feat: improve page component & update AuthUI path * chore: update bits-ui to version 2.14.4 and all org related UI's - Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4. - Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory. - Removed unused Course components and related files to clean up the codebase. - Enhanced various components to improve consistency and maintainability. * chore: move all setting buttons to the top and update auth ui styles * chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner - Removed all instances of svelte-loading-spinners from the project. - Updated components to use the new Spinner from @cio/ui/base/spinner. - Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners. - Added new CSS animations for rocket launch effects in app.css. - Enhanced email change flow with updated verification messages in translations. * chore: remove supabase file upload from dashboard and move to r2 * chore: update organization API and UI components - Added new API methods for organization plan operations and team management. - Introduced middleware for API key authentication and organization admin checks. - Refactored organization-related components to utilize the new API structure. - Updated various UI components to improve consistency and maintainability. - Enhanced error handling and validation for organization-related actions. - Removed deprecated services and streamlined the codebase.
199 lines
5.7 KiB
TypeScript
199 lines
5.7 KiB
TypeScript
#!/usr/bin/env tsx
|
|
|
|
import 'dotenv/config';
|
|
|
|
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
import { join, relative } from 'path';
|
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
|
|
interface UploadOptions {
|
|
key: string;
|
|
content: Buffer | string;
|
|
contentType?: string;
|
|
}
|
|
|
|
class StorybookUploader {
|
|
private s3Client: S3Client | null = null;
|
|
private buildDir: string;
|
|
private bucket: string;
|
|
|
|
constructor() {
|
|
this.buildDir = join(process.cwd(), 'storybook-static');
|
|
this.bucket = 'assets';
|
|
this.initializeS3Client();
|
|
}
|
|
|
|
private initializeS3Client() {
|
|
const accessKey = process.env.CLOUDFLARE_ACCESS_KEY;
|
|
const secretKey = process.env.CLOUDFLARE_SECRET_ACCESS_KEY;
|
|
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
|
|
if (accessKey && secretKey && accountId) {
|
|
this.s3Client = new S3Client({
|
|
region: 'auto',
|
|
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
credentials: {
|
|
accessKeyId: accessKey,
|
|
secretAccessKey: secretKey
|
|
}
|
|
});
|
|
console.log('✅ S3 client initialized for Cloudflare R2');
|
|
} else {
|
|
console.warn('⚠️ Cloudflare R2 credentials not found. Cannot upload to R2.');
|
|
throw new Error('Missing Cloudflare R2 credentials');
|
|
}
|
|
}
|
|
|
|
private getContentType(filePath: string): string {
|
|
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
const contentTypes: Record<string, string> = {
|
|
html: 'text/html',
|
|
js: 'application/javascript',
|
|
json: 'application/json',
|
|
css: 'text/css',
|
|
png: 'image/png',
|
|
jpg: 'image/jpeg',
|
|
jpeg: 'image/jpeg',
|
|
gif: 'image/gif',
|
|
svg: 'image/svg+xml',
|
|
ico: 'image/x-icon',
|
|
woff: 'font/woff',
|
|
woff2: 'font/woff2',
|
|
ttf: 'font/ttf',
|
|
eot: 'application/vnd.ms-fontobject',
|
|
map: 'application/json',
|
|
txt: 'text/plain',
|
|
xml: 'application/xml'
|
|
};
|
|
return contentTypes[ext || ''] || 'application/octet-stream';
|
|
}
|
|
|
|
private getCacheControl(filePath: string): string {
|
|
// Cache static assets longer, HTML files shorter
|
|
if (filePath.endsWith('.html')) {
|
|
return 'public, max-age=300'; // 5 minutes for HTML
|
|
}
|
|
if (filePath.match(/\.(js|css|woff|woff2|ttf|png|jpg|jpeg|gif|svg|ico)$/)) {
|
|
return 'public, max-age=31536000, immutable'; // 1 year for assets
|
|
}
|
|
return 'public, max-age=3600'; // 1 hour default
|
|
}
|
|
|
|
private getAllFiles(dir: string, baseDir: string = dir): string[] {
|
|
const files: string[] = [];
|
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = join(dir, entry.name);
|
|
const relativePath = relative(baseDir, fullPath);
|
|
|
|
if (entry.isDirectory()) {
|
|
files.push(...this.getAllFiles(fullPath, baseDir));
|
|
} else {
|
|
files.push(relativePath);
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
async uploadFile(filePath: string): Promise<void> {
|
|
if (!this.s3Client) {
|
|
throw new Error('S3 client not initialized. Check your Cloudflare R2 credentials.');
|
|
}
|
|
|
|
try {
|
|
const fullPath = join(this.buildDir, filePath);
|
|
const content = readFileSync(fullPath);
|
|
const contentType = this.getContentType(filePath);
|
|
const cacheControl = this.getCacheControl(filePath);
|
|
|
|
// Upload to storybook/ directory in R2
|
|
const key = `storybook/${filePath}`;
|
|
|
|
const command = new PutObjectCommand({
|
|
Bucket: this.bucket,
|
|
Key: key,
|
|
Body: content,
|
|
ContentType: contentType,
|
|
CacheControl: cacheControl
|
|
});
|
|
|
|
await this.s3Client.send(command);
|
|
console.log(`✅ Uploaded: ${filePath}`);
|
|
} catch (error) {
|
|
console.error(`❌ Error uploading ${filePath}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async uploadAll(): Promise<void> {
|
|
try {
|
|
// Check if build directory exists
|
|
try {
|
|
statSync(this.buildDir);
|
|
} catch {
|
|
throw new Error(`Build directory not found: ${this.buildDir}. Please run 'pnpm build' first.`);
|
|
}
|
|
|
|
console.log('🔄 Starting Storybook upload to R2...');
|
|
console.log(`📁 Build directory: ${this.buildDir}`);
|
|
|
|
const files = this.getAllFiles(this.buildDir);
|
|
console.log(`📦 Found ${files.length} files to upload`);
|
|
|
|
// Upload files in batches to avoid overwhelming the API
|
|
const batchSize = 10;
|
|
for (let i = 0; i < files.length; i += batchSize) {
|
|
const batch = files.slice(i, i + batchSize);
|
|
await Promise.all(batch.map((file) => this.uploadFile(file)));
|
|
console.log(`📤 Uploaded batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(files.length / batchSize)}`);
|
|
}
|
|
|
|
console.log('🎉 Storybook upload completed successfully!');
|
|
console.log(`📊 Total files uploaded: ${files.length}`);
|
|
} catch (error) {
|
|
console.error('❌ Failed to upload Storybook:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CLI interface
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(`
|
|
Usage: tsx scripts/upload-storybook.ts
|
|
|
|
Examples:
|
|
tsx scripts/upload-storybook.ts
|
|
|
|
Environment Variables Required for R2 Upload:
|
|
CLOUDFLARE_ACCESS_KEY
|
|
CLOUDFLARE_SECRET_ACCESS_KEY
|
|
CLOUDFLARE_ACCOUNT_ID
|
|
`);
|
|
return;
|
|
}
|
|
|
|
const uploader = new StorybookUploader();
|
|
await uploader.uploadAll();
|
|
}
|
|
|
|
// Run the script if called directly
|
|
if (process.argv[1] && process.argv[1].endsWith('upload-storybook.ts')) {
|
|
main()
|
|
.then(() => {
|
|
console.log('Script completed successfully');
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Script failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
export { StorybookUploader };
|