Skip to content

Commit

Permalink
Merge branch 'main' into simplify-grouped-queue-2
Browse files Browse the repository at this point in the history
  • Loading branch information
tonsky authored Feb 20, 2025
2 parents 87792d6 + 02787ce commit c52ff9e
Show file tree
Hide file tree
Showing 29 changed files with 745 additions and 567 deletions.
140 changes: 140 additions & 0 deletions client/sandbox/react-nextjs/pages/play/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
'use client';

import { init, tx } from '@instantdb/react';
import React, { useState, useEffect } from 'react';
import Login from '../../components/Login';
import config from '../../config';

const db = init(config);

interface AvatarUploadProps {
defaultSize?: number;
}

function AvatarUpload({ defaultSize = 96 }: AvatarUploadProps) {
const [isUploading, setIsUploading] = useState(false);
const { user } = db.useAuth();
const {
isLoading: isLoadingAvatar,
data,
error: avatarError,
} = db.useQuery(
user
? {
profiles: {
$: {
where: { '$user.id': user.id },
},
avatar: {},
},
}
: null,
);
if (isLoadingAvatar) return <div>Loading...</div>;
if (avatarError) return <div>Error: {avatarError.message}</div>;

const profile = data.profiles[0];
const { id: profileId } = profile;
const avatar = profile.avatar[0];

const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

try {
setIsUploading(true);
// We set an explicit path to make sure that when users change
// their avatar we upload to the same path. This way we keep
// the same URL for the avatar.
//
// We set the profileId in the path for permission checks. This
// way we can write a rule to ensure that only the user can
// upload to their own profile.
const path = `avatars/${profileId}/avatar`;

const { data } = await db.storage.uploadFile(path, file);

// Link the file to the profile
await db.transact(tx.profiles[profileId].link({ avatar: data.id }));
} catch (error) {
console.error('Error uploading avatar:', error);
} finally {
setIsUploading(false);
}
};

return (
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div style={{ width: defaultSize, height: defaultSize }}>
{avatar ? (
<img src={avatar.url} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-gray-400" />
)}
</div>

{isUploading && (
<div className="absolute inset-0 bg-black bg-opacity-40 rounded-full flex items-center justify-center">
<div className="w-8 h-8 border-4 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>

<label className="cursor-pointer">
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
<span className="py-2 px-4 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm transition-colors">
{avatar ? 'Change Avatar' : 'Upload Avatar'}
</span>
</label>
</div>
);
}

function ProfilePage() {
return (
<div className="box-border bg-gray-50 font-mono min-h-screen p-5 flex items-center flex-col">
<div className="tracking-wider text-3xl text-gray-700 mb-8">
Profile Settings
</div>

<div className="bg-white rounded-lg shadow-md p-8 max-w-2xl w-full">
<h2 className="text-xl mb-6 pb-2 border-b border-gray-200">
Profile Picture
</h2>

<div className="flex justify-center">
<AvatarUpload defaultSize={120} />
</div>
</div>
<button
className="text-sm text-gray-500 mt-2"
onClick={() => db.auth.signOut()}
>
{' '}
Sign out
</button>
</div>
);
}

function Page() {
const { isLoading, user, error } = db.useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Uh oh! {error.message}</div>;
}
if (user) {
return <ProfilePage />;
}
return <Login auth={db.auth} />;
}

export default Page;
123 changes: 31 additions & 92 deletions client/sandbox/react-nextjs/pages/play/storage-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ function App() {
}
const { $files: images } = data as { $files: Image[] };
return (
<div style={styles.container}>
<div style={styles.header}>Image Feed</div>
<div className="box-border bg-gray-50 font-mono min-h-screen p-5 flex items-center flex-col">
<div className="tracking-wider text-5xl text-gray-300 mb-8">
Image Feed
</div>
<ImageUpload />
<ImageGrid images={images} />
</div>
Expand Down Expand Up @@ -104,17 +106,24 @@ function ImageUpload() {
};

return (
<div style={styles.uploadContainer}>
<div className="mb-8 p-5 border-2 border-dashed border-gray-300 rounded-lg">
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
style={styles.fileInput}
className="font-mono"
/>
{previewURL && (
<div style={styles.previewContainer}>
<img src={previewURL} alt="Preview" style={styles.previewImage} />
<button onClick={handleUpload} style={styles.uploadButton}>
<div className="mt-5 flex flex-col items-center gap-3">
<img
src={previewURL}
alt="Preview"
className="max-w-xs max-h-xs object-contain"
/>
<button
onClick={handleUpload}
className="py-2 px-4 bg-green-500 text-white border-none rounded cursor-pointer font-mono"
>
Upload Image
</button>
</div>
Expand All @@ -125,13 +134,23 @@ function ImageUpload() {

function ImageGrid({ images }: { images: Image[] }) {
return (
<div style={styles.imageGrid}>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full max-w-6xl">
{images.map((image, idx) => (
<div key={image.id} style={styles.imageContainer}>
<img src={image.url} alt={image.path} style={styles.image} />
<div style={styles.imageCaption}>
<div
key={image.id}
className="border border-gray-300 rounded-lg overflow-hidden"
>
<img
src={image.url}
alt={image.path}
className="w-full h-64 object-cover"
/>
<div className="p-3 flex justify-between items-center bg-white">
<span>{image.path}</span>
<span onClick={() => deleteImage(image)} style={styles.delete}>
<span
onClick={() => deleteImage(image)}
className="cursor-pointer text-gray-300 px-1"
>
𝘟
</span>
</div>
Expand All @@ -141,84 +160,4 @@ function ImageGrid({ images }: { images: Image[] }) {
);
}

// Styles
// ----------
const styles: Record<string, React.CSSProperties> = {
previewContainer: {
marginTop: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
},
previewImage: {
maxWidth: '200px',
maxHeight: '200px',
objectFit: 'contain',
},
uploadButton: {
padding: '8px 16px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontFamily: 'code, monospace',
},
container: {
boxSizing: 'border-box',
backgroundColor: '#fafafa',
fontFamily: 'code, monospace',
minHeight: '100vh',
padding: '20px',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
},
header: {
letterSpacing: '2px',
fontSize: '50px',
color: 'lightgray',
marginBottom: '30px',
},
uploadContainer: {
marginBottom: '30px',
padding: '20px',
border: '2px dashed lightgray',
borderRadius: '8px',
},
fileInput: {
fontFamily: 'code, monospace',
},
imageGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '20px',
width: '100%',
maxWidth: '1200px',
},
imageContainer: {
border: '1px solid lightgray',
borderRadius: '8px',
overflow: 'hidden',
},
image: {
width: '100%',
height: '300px',
objectFit: 'cover',
},
imageCaption: {
padding: '10px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
},
delete: {
cursor: 'pointer',
color: 'lightgray',
padding: '0 5px',
},
};

export default Wrapper;
60 changes: 0 additions & 60 deletions client/www/components/dash/Storage.tsx

This file was deleted.

8 changes: 1 addition & 7 deletions client/www/components/dash/explorer/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,9 @@ function SearchInput({
export function Explorer({
db,
appId,
isStorageEnabled,
}: {
db: InstantReactWebDatabase<any>;
appId: string;
isStorageEnabled: boolean;
}) {
// DEV
_dev(db);
Expand Down Expand Up @@ -452,11 +450,7 @@ export function Explorer({
}

// data
const { namespaces: _namespaces } = useSchemaQuery(db);
// (TODO): When fully launching storage we can remove this check
const namespaces = isStorageEnabled
? _namespaces
: _namespaces && _namespaces.filter((ns) => ns.name !== '$files');
const { namespaces } = useSchemaQuery(db);
const { selectedNamespace } = useMemo(
() => ({
selectedNamespace: namespaces?.find(
Expand Down
2 changes: 1 addition & 1 deletion client/www/data/docsNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ module.exports = [
{ title: 'Devtool', href: '/docs/devtool' },
{ title: 'Custom emails', href: '/docs/emails' },
{ title: 'App teams', href: '/docs/teams' },
{ title: 'Storage (beta)', href: '/docs/storage' },
{ title: 'Storage', href: '/docs/storage' },
],
},
];
Loading

0 comments on commit c52ff9e

Please sign in to comment.