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
6 changes: 3 additions & 3 deletions packages/db/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Export all schemas, avoiding conflicts
export * from "./auth";
export * from "./users";
export * from "./members";
export * from "./members"; // This includes userProfiles
export * from "./hackathons";
export * from "./admins";
export * from "./events";
export * from "./events";
15 changes: 0 additions & 15 deletions packages/db/src/schemas/users.ts

This file was deleted.

104 changes: 93 additions & 11 deletions sites/portal/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,68 @@ export default function Dashboard() {
</div>

<div className="flex gap-4 border-b border-white/5 mb-6">
<button onClick={() => setEditTab('basic')} className={`pb-2 text-[10px] uppercase tracking-widest transition-all ${editTab === 'basic' ? 'text-[#00A8A8] border-b border-[#00A8A8]' : 'text-gray-600'}`}>Basic_Identity (Public)</button>
<button
onClick={() => memberStatus?.isMember && setEditTab('member')}
className={`pb-2 text-[10px] uppercase tracking-widest transition-all ${!memberStatus?.isMember ? 'opacity-20 cursor-not-allowed' : ''} ${editTab === 'member' ? 'text-[#00A8A8] border-b border-[#00A8A8]' : 'text-gray-600'}`}
onClick={() => setEditTab('basic')}
className={`pb-2 text-[10px] uppercase tracking-widest transition-all ${editTab === 'basic' ? 'text-[#00A8A8] border-b border-[#00A8A8]' : 'text-gray-600'}`}
>
Member_Dossier {!memberStatus?.isMember && '🔒'}
Basic_Identity
</button>
<button
onClick={() => setEditTab('member')}
className={`pb-2 text-[10px] uppercase tracking-widest transition-all ${!memberStatus?.isMember ? 'opacity-40' : ''} ${editTab === 'member' ? 'text-[#00A8A8] border-b border-[#00A8A8]' : 'text-gray-600'}`}
>
Advanced_Logs {!memberStatus?.isMember && '(Members Only)'}
</button>
</div>

{editTab === 'basic' && userData && <ProfileForm user={userData as any} />}
{editTab === 'basic' && userData && (
<div className="space-y-6">
<ProfileForm user={userData as any} />

{/* Guest Profile Editor for interests/skills */}
{!memberStatus?.isMember && (
<div className="border-t border-white/10 pt-6 mt-6">
<div className="bg-amber-500/5 border border-amber-500/20 rounded-lg p-4 mb-4">
<p className="text-amber-500 text-[10px] uppercase tracking-widest mb-2">💡 Limited Profile Access</p>
<p className="text-gray-400 text-xs">
As a guest, you can edit basic info. To unlock advanced member features (academic info, detailed portfolio), please register as a member.
</p>
<button
onClick={() => {
setIsEditing(false);
router.push('/member/register');
}}
className="mt-3 px-4 py-2 bg-amber-500/10 border border-amber-500/30 text-amber-500 text-[9px] uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all"
>
Register_As_Member →
</button>
</div>
</div>
)}
</div>
)}

{editTab === 'member' && (
memberStatus?.isMember ? <MemberForm member={memberData as any} /> : <p className="text-center py-10 text-[10px] uppercase text-amber-500">Membership_Required_For_Advanced_Logs</p>
memberStatus?.isMember ? (
<MemberForm member={memberData as any} />
) : (
<div className="text-center py-12 space-y-4">
<div className="text-6xl mb-4">🔒</div>
<p className="text-amber-500 text-xs uppercase tracking-widest">Members_Only_Section</p>
<p className="text-gray-500 text-[10px] max-w-md mx-auto">
Advanced member logs include academic records, detailed skill tracking, and official membership credentials. Register to unlock full access.
</p>
<button
onClick={() => {
setIsEditing(false);
router.push('/member/register');
}}
className="mt-6 px-6 py-3 bg-amber-500/10 border border-amber-500/30 text-amber-500 text-[10px] uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all"
>
Complete_Registration →
</button>
</div>
)
)}
</div>
</div>
Expand Down Expand Up @@ -132,7 +182,7 @@ export default function Dashboard() {
<p className="text-xs text-white leading-relaxed italic font-mono min-h-[40px]">
"{userData?.bio || "No public bio transmission found."}"
</p>
<button onClick={() => setIsEditing(true)} className="text-[#00A8A8] text-[9px] uppercase tracking-widest hover:underline pt-2">Modify_Identity →</button>
<button onClick={() => { setEditTab('basic'); setIsEditing(true); }} className="text-[#00A8A8] text-[9px] uppercase tracking-widest hover:underline pt-2">Modify_Identity →</button>
</div>
</div>

Expand All @@ -142,23 +192,41 @@ export default function Dashboard() {
<div className="space-y-4">
<p className="text-[9px] text-gray-600 uppercase tracking-widest font-mono italic">/Academic_Logs</p>
<div className="text-[10px] font-mono space-y-2">
<p><span className="text-gray-700">INSTITUTION:</span> {memberData?.school}</p>
<p><span className="text-gray-700">PROGRAM:</span> {memberData?.major}</p>
<p><span className="text-gray-700">CYCLE_END:</span> {memberData?.graduationYear}</p>
<p><span className="text-gray-700">INSTITUTION:</span> {memberData?.school || 'N/A'}</p>
<p><span className="text-gray-700">PROGRAM:</span> {memberData?.major || 'N/A'}</p>
<p><span className="text-gray-700">CYCLE_END:</span> {memberData?.graduationYear || 'N/A'}</p>
</div>
</div>
<div>
<p className="text-[9px] text-gray-600 uppercase tracking-widest font-mono italic">/Neural_Links</p>
<div className="flex flex-wrap gap-3 mt-3">
{memberData?.githubUrl && <a href={memberData.githubUrl} target="_blank" className="text-white border border-white/10 px-3 py-1 text-[9px] hover:bg-white hover:text-black transition-all">GITHUB</a>}
{memberData?.linkedinUrl && <a href={memberData.linkedinUrl} target="_blank" className="text-white border border-white/10 px-3 py-1 text-[9px] hover:bg-white hover:text-black transition-all">LINKEDIN</a>}
{memberData?.portfolioUrl && <a href={memberData.portfolioUrl} target="_blank" className="text-white border border-white/10 px-3 py-1 text-[9px] hover:bg-white hover:text-black transition-all">PORTFOLIO</a>}
</div>
</div>
<div className="md:col-span-2">
<p className="text-[9px] text-gray-600 uppercase tracking-widest font-mono italic mb-3">/Skills_&_Interests</p>
<div className="flex flex-wrap gap-2">
{memberData?.skills?.map((skill: string, i: number) => (
<span key={i} className="px-2 py-1 bg-[#00A8A8]/10 border border-[#00A8A8]/30 text-[#00A8A8] text-[9px] uppercase">{skill}</span>
))}
{memberData?.interests?.map((interest: string, i: number) => (
<span key={i} className="px-2 py-1 bg-white/5 border border-white/10 text-white text-[9px] uppercase">{interest}</span>
))}
</div>
</div>
</div>
) : (
<div className="p-10 border border-white/5 bg-white/[0.01] rounded-xl text-center">
<div className="p-10 border border-white/5 bg-white/[0.01] rounded-xl text-center space-y-4">
<p className="text-gray-700 font-mono text-[9px] uppercase tracking-[0.4em]">Advanced_Dossier_Encrypted</p>
<p className="text-[8px] text-gray-800 mt-2 uppercase">Complete Membership Registration to Unlock Verified Logs</p>
<button
onClick={() => router.push('/member/register')}
className="mt-4 px-6 py-2 border border-amber-500/30 text-amber-500 text-[10px] uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all"
>
Register_Member →
</button>
</div>
)}
</div>
Expand All @@ -175,11 +243,25 @@ export default function Dashboard() {
) : (
<div className="py-12 border border-amber-500/20 bg-amber-500/5 rounded-xl text-center space-y-4">
<p className="text-amber-500 font-mono text-xs uppercase tracking-widest">ERR: Unregistered_Node</p>
<p className="text-gray-500 text-[10px] max-w-md mx-auto">Club operations require active membership. Register to access events, check-ins, and member benefits.</p>
<button onClick={() => router.push('/member/register')} className="px-8 py-2 border border-amber-500/30 text-amber-500 text-[10px] uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all">Register_Member</button>
</div>
)}
</div>
)}

{/* --- HACKLYTICS MODE --- */}
{mode === 'HACKLYTICS' && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4">
<div className="text-center py-10 space-y-4">
<p className="text-amber-500 font-mono text-[10px] tracking-widest">Hackathon_Portal_Active</p>
<p className="text-gray-500 text-xs max-w-md mx-auto">Browse hackathons, register for events, and submit projects. All users (members and guests) can participate.</p>
<button onClick={() => router.push('/hackathons')} className="px-8 py-3 bg-amber-500/10 border border-amber-500/30 text-amber-500 text-[10px] uppercase tracking-widest hover:bg-amber-500 hover:text-black transition-all">
View_Hackathons →
</button>
</div>
</div>
)}
</div>
</div>
</main>
Expand Down
160 changes: 128 additions & 32 deletions sites/portal/src/components/profile/ProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,150 @@

import { useState } from 'react';
import { trpc } from '@/lib/trpc';
import Image from 'next/image';
import { useRouter } from 'next/navigation';

export default function ProfileForm({ user }: { user: { name?: string | null; image?: string | null } }) {
interface ProfileFormProps {
user: {
id: string;
name?: string | null;
email: string;
image?: string | null;
bio?: string | null;
};
}

export default function ProfileForm({ user }: ProfileFormProps) {
const router = useRouter();
const utils = trpc.useUtils();
const [name, setName] = useState(user.name || '');
const [imageUrl, setImageUrl] = useState(user.image || '');

const [formData, setFormData] = useState({
name: user.name || '',
image: user.image || '',
bio: user.bio || '',
});

const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);

const updateProfile = trpc.user.updateProfile.useMutation({
onSuccess: () => utils.user.me.invalidate(),
onSuccess: () => {
setMessage({ type: 'success', text: 'Profile updated successfully' });
utils.user.me.invalidate();
setIsSubmitting(false);

setTimeout(() => setMessage(null), 3000);
},
onError: (error) => {
setMessage({ type: 'error', text: error.message || 'Failed to update profile' });
setIsSubmitting(false);
},
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateProfile.mutate({ name, image: imageUrl });
const handleSubmit = () => {
setIsSubmitting(true);
setMessage(null);

updateProfile.mutate({
name: formData.name || undefined,
image: formData.image || undefined,
bio: formData.bio || undefined,
});
};

return (
<form onSubmit={handleSubmit} className="space-y-6 animate-in fade-in duration-500">
<div className="flex items-center gap-6 p-4 bg-white/5 rounded-xl border border-white/5">
<Image
src={imageUrl || '/avatar-placeholder.png'}
alt="Preview"
width={64} height={64}
className="rounded-full border-2 border-[#00A8A8] object-cover bg-black"
<div className="space-y-6">
{message && (
<div className={`p-4 rounded-lg border ${
message.type === 'success'
? 'bg-[#00A8A8]/10 border-[#00A8A8]/30 text-[#00A8A8]'
: 'bg-red-500/10 border-red-500/30 text-red-500'
}`}>
<p className="text-[10px] uppercase tracking-widest">{message.text}</p>
</div>
)}

<div className="space-y-2">
<label className="text-[9px] text-gray-500 uppercase tracking-widest font-mono">
Display_Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter your name"
className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-3 text-white text-sm focus:border-[#00A8A8] focus:outline-none transition-all"
maxLength={100}
/>
<div className="flex-1 space-y-1">
<label className="text-[9px] uppercase tracking-widest text-gray-500">Profile_Image_URL (.jpg/.png)</label>
<input
value={imageUrl}
onChange={e => setImageUrl(e.target.value)}
placeholder="https://example.com/photo.jpg"
className="w-full bg-black/40 border border-white/10 rounded p-2 text-xs text-white focus:border-[#00A8A8]/50 outline-none"
/>
<p className="text-[8px] text-gray-700 uppercase">Visible to all users</p>
</div>

<div className="space-y-2">
<label className="text-[9px] text-gray-500 uppercase tracking-widest font-mono">
Avatar_URL
</label>
<input
type="url"
value={formData.image}
onChange={(e) => setFormData({ ...formData, image: e.target.value })}
placeholder="https://example.com/avatar.jpg"
className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-3 text-white text-sm focus:border-[#00A8A8] focus:outline-none transition-all font-mono"
/>
<p className="text-[8px] text-gray-700 uppercase">Direct link to JPG/PNG image</p>
</div>

<div className="space-y-2">
<label className="text-[9px] text-gray-500 uppercase tracking-widest font-mono">
Public_Bio
</label>
<textarea
value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
placeholder="Tell the community about yourself..."
rows={4}
className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-3 text-white text-sm focus:border-[#00A8A8] focus:outline-none transition-all resize-none"
maxLength={500}
/>
<div className="flex justify-between items-center">
<p className="text-[8px] text-gray-700 uppercase">Visible on your profile</p>
<p className="text-[8px] text-gray-600 font-mono">{formData.bio.length}/500</p>
</div>
</div>

<div className="space-y-1">
<label className="text-[9px] uppercase tracking-widest text-gray-500">Display_Name</label>
<div className="space-y-2">
<label className="text-[9px] text-gray-500 uppercase tracking-widest font-mono">
Email_Address
</label>
<input
value={name}
onChange={e => setName(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded p-3 text-sm text-white focus:border-[#00A8A8]/50 outline-none"
type="email"
value={user.email}
disabled
className="w-full bg-black/20 border border-white/5 rounded-lg px-4 py-3 text-gray-600 text-sm cursor-not-allowed font-mono"
/>
<p className="text-[8px] text-gray-700 uppercase">Cannot be modified</p>
</div>

<button type="submit" disabled={updateProfile.isPending} className="w-full py-4 bg-white/10 text-white border border-white/10 uppercase font-black text-[10px] tracking-widest hover:bg-white hover:text-black transition-all">
{updateProfile.isPending ? 'Uploading_Data...' : 'Update_Basic_Node'}
</button>
</form>
<div className="bg-white/5 border border-white/10 rounded-lg p-4">
<p className="text-[9px] text-gray-500 uppercase tracking-widest mb-2">💡 Profile Tips</p>
<ul className="text-[10px] text-gray-400 space-y-1">
<li>• Use a clear profile picture for better recognition</li>
<li>• Keep your bio concise and professional</li>
<li>• Your email is private and used only for authentication</li>
</ul>
</div>

<div className="flex gap-3 pt-4">
<button
onClick={handleSubmit}
disabled={isSubmitting}
className={`flex-1 py-3 bg-[#00A8A8] text-black font-bold uppercase text-[10px] tracking-widest rounded-lg transition-all ${
isSubmitting
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-[#00A8A8]/80'
}`}
>
{isSubmitting ? 'Updating...' : 'Save_Changes'}
</button>
</div>
</div>
);
}
Loading