Portfolio: add filtering, URL sharing, and improved tag input
- Add category filters with dynamic counts that reduce as filters applied - Add ?filter[category]=value URL params for filterable links - Add ?img= timestamp param that bypasses filters to show specific image - Update URL when opening/navigating/closing modal for shareable links - Backend: add /api/portfolio/filters endpoint with filter logic - Backend: add timestamp lookup fallback for GetImage endpoint Frontend: - Portfolio page: filter dropdowns, keyboard nav, mobile improvements - ImageUpload: live tag suggestions from API, arrow/Tab navigation, confirmation modal before upload, mobile-optimized touch targets - Add scrollbar-hide utility and fix filter dropdown overflow - Move Clear all button, add vertical separator on desktop
This commit is contained in:
@@ -32,29 +32,48 @@ type S3Client struct {
|
||||
}
|
||||
|
||||
func Connect() error {
|
||||
endpoint := os.Getenv("S3_ENDPOINT")
|
||||
// Check for RUSTFS_* vars first (matching compose.yml), fall back to S3_* vars
|
||||
endpoint := os.Getenv("RUSTFS_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
endpoint = os.Getenv("S3_ENDPOINT")
|
||||
}
|
||||
if endpoint == "" {
|
||||
// Default: localhost for bare metal dev, use rustfs:9000 for docker
|
||||
endpoint = "http://localhost:9000"
|
||||
}
|
||||
|
||||
accessKey := os.Getenv("S3_ACCESS_KEY")
|
||||
accessKey := os.Getenv("RUSTFS_ACCESS_KEY")
|
||||
if accessKey == "" {
|
||||
accessKey = os.Getenv("S3_ACCESS_KEY")
|
||||
}
|
||||
if accessKey == "" {
|
||||
accessKey = "minioadmin"
|
||||
}
|
||||
|
||||
secretKey := os.Getenv("S3_SECRET_KEY")
|
||||
secretKey := os.Getenv("RUSTFS_SECRET_KEY")
|
||||
if secretKey == "" {
|
||||
secretKey = os.Getenv("S3_SECRET_KEY")
|
||||
}
|
||||
if secretKey == "" {
|
||||
secretKey = "minioadmin"
|
||||
}
|
||||
|
||||
bucket := os.Getenv("S3_BUCKET")
|
||||
bucket := os.Getenv("RUSTFS_BUCKET")
|
||||
if bucket == "" {
|
||||
bucket = os.Getenv("S3_BUCKET")
|
||||
}
|
||||
if bucket == "" {
|
||||
bucket = "crussell"
|
||||
}
|
||||
|
||||
region := os.Getenv("AWS_REGION")
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
region = "eu-west-2"
|
||||
}
|
||||
|
||||
publicURL := os.Getenv("S3_PUBLIC_URL")
|
||||
if publicURL == "" {
|
||||
publicURL = endpoint
|
||||
}
|
||||
|
||||
awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
|
||||
@@ -75,7 +94,39 @@ func Connect() error {
|
||||
o.UsePathStyle = true
|
||||
}),
|
||||
bucket: bucket,
|
||||
publicURL: endpoint,
|
||||
publicURL: publicURL,
|
||||
}
|
||||
|
||||
// Create bucket if it doesn't exist
|
||||
ctx := context.Background()
|
||||
s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.BaseEndpoint = aws.String(endpoint)
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
_, err = s3Client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Bucket creation: %v (may already exist)", err)
|
||||
}
|
||||
|
||||
// Set bucket policy for public read access
|
||||
policy := fmt.Sprintf(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Sid": "PublicReadGetObject",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::%s/*"
|
||||
}]
|
||||
}`, bucket)
|
||||
_, err = s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Policy: aws.String(policy),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Bucket policy: %v (may already exist)", err)
|
||||
}
|
||||
|
||||
log.Printf("Connected to local S3 (Rustfs): bucket=%s, endpoint=%s", bucket, endpoint)
|
||||
@@ -84,9 +135,10 @@ func Connect() error {
|
||||
|
||||
func (s *S3Client) Upload(ctx context.Context, bucket, key string, body io.Reader) error {
|
||||
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: body,
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(key),
|
||||
Body: body,
|
||||
ContentType: aws.String("image/jpeg"),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user