feat(profile): enhance user profile editing capabilities

- Added fields for bio, website, and social media links to the EditProfileScreen.
- Updated database schema to include new fields for user profiles.
- Modified UserAvatar component to adjust font size dynamically.
- Expanded environment options in debug settings to include 'local'.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-11 18:35:04 +08:00
parent 68e1c24119
commit 95616e2d6c
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
10 changed files with 166 additions and 7 deletions

View File

@ -37,7 +37,7 @@ export const UserAvatar = ({
{name ? (
<Text
className="text-secondary-label p-2 text-center uppercase"
style={{ fontSize: size }}
style={{ fontSize: size / 3 }}
adjustsFontSizeToFit
>
{name.slice(0, 2)}

View File

@ -98,7 +98,7 @@ export const EnvProfileIndicator = () => {
</View>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{["prod", "dev", "staging"].map((env) => {
{["prod", "dev", "staging", "local"].map((env) => {
return (
<DropdownMenu.Item
key={env}

View File

@ -20,6 +20,7 @@ import {
GroupedInsetListCard,
GroupedInsetListCell,
GroupedInsetListNavigationLink,
GroupedInsetListSectionHeader,
GroupedOutlineDescription,
} from "@/src/components/ui/grouped/GroupedList"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
@ -34,6 +35,7 @@ import { setAvatar } from "../utils"
export const EditProfileScreen = () => {
const whoami = useWhoami()
const { t } = useTranslation("settings")
const [dirtyFields, setDirtyFields] = useState<Partial<UserProfileEditable>>({})
@ -111,6 +113,25 @@ const ProfileForm: FC<{
const { t } = useTranslation("settings")
const navigation = useNavigation()
const socialLinkFields: (keyof NonNullable<MeModel["socialLinks"]>)[] = [
"twitter",
"github",
"instagram",
"facebook",
"youtube",
"discord",
]
const socialCopyMap = {
twitter: t("profile.social.twitter", "Twitter"),
github: t("profile.social.github", "GitHub"),
instagram: t("profile.social.instagram", "Instagram"),
facebook: t("profile.social.facebook", "Facebook"),
youtube: t("profile.social.youtube", "YouTube"),
discord: t("profile.social.discord", "Discord"),
}
return (
<View className="mt-4">
<TouchableWithoutFeedback
@ -160,8 +181,6 @@ const ProfileForm: FC<{
</View>
</GroupedInsetListCell>
</GroupedInsetListCard>
<GroupedOutlineDescription description={t("profile.handle.description")} />
{/* Email */}
<GroupedInsetListCard className="mt-4">
<GroupedInsetListNavigationLink
@ -183,6 +202,86 @@ const ProfileForm: FC<{
}
/>
</GroupedInsetListCard>
<GroupedOutlineDescription description={t("profile.handle.description")} />
<GroupedInsetListSectionHeader label={t("profile.bio.label", "Bio")} />
<GroupedInsetListCard>
<View className="flex-1 px-4 py-3">
<PlainTextField
className="text-text h-[100px] w-full flex-1"
value={dirtyFields.bio ?? whoami?.bio ?? ""}
hitSlop={10}
multiline
selectionColor={accentColor}
onChangeText={(text) => {
setDirtyFields({ ...dirtyFields, bio: text })
}}
textAlignVertical="top"
placeholder={t("profile.bio.placeholder", "Tell us about yourself")}
/>
</View>
</GroupedInsetListCard>
{/* Website */}
<GroupedInsetListCard className="mt-4">
<GroupedInsetListCell
label={t("profile.website.label", "Website")}
leftClassName="flex-none"
rightClassName="flex-1"
>
<View className="flex-1">
<PlainTextField
className="text-secondary-label w-full flex-1 text-right"
value={dirtyFields.website ?? whoami?.website ?? ""}
hitSlop={10}
selectionColor={accentColor}
onChangeText={(text) => {
setDirtyFields({ ...dirtyFields, website: text })
}}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
placeholder="https://example.com"
/>
</View>
</GroupedInsetListCell>
</GroupedInsetListCard>
{/* Social Links */}
<GroupedInsetListSectionHeader
label={t("profile.social.title", "Social Media Handles")}
/>
<GroupedInsetListCard>
{socialLinkFields.map((social) => (
<GroupedInsetListCell
key={social}
label={socialCopyMap[social]}
leftClassName="flex-none"
rightClassName="flex-1"
>
<View className="flex-1">
<PlainTextField
className="text-secondary-label w-full flex-1 text-right"
value={dirtyFields.socialLinks?.[social] ?? whoami?.socialLinks?.[social] ?? ""}
hitSlop={10}
selectionColor={accentColor}
onChangeText={(text) => {
setDirtyFields({
...dirtyFields,
socialLinks: {
...whoami?.socialLinks,
...dirtyFields.socialLinks,
[social]: text,
},
})
}}
autoCapitalize="none"
autoCorrect={false}
/>
</View>
</GroupedInsetListCell>
))}
</GroupedInsetListCard>
</View>
</TouchableWithoutFeedback>
</View>

View File

@ -176,6 +176,7 @@ export const DebugScreen: NavigationControllerView = () => {
{ label: "Dev", value: "dev" },
{ label: "Prod", value: "prod" },
{ label: "Staging", value: "staging" },
{ label: "Local", value: "local" },
]}
value={envProfile}
onValueChange={(value) => {

View File

@ -70,6 +70,16 @@ export const usersTable = sqliteTable("users", {
image: text("image"),
isMe: integer("is_me", { mode: "boolean" }),
emailVerified: integer("email_verified", { mode: "boolean" }),
bio: text("bio"),
website: text("website"),
socialLinks: text("social_links", { mode: "json" }).$type<{
twitter?: string
github?: string
instagram?: string
facebook?: string
youtube?: string
discord?: string
}>(),
})
export const entriesTable = sqliteTable("entries", {

View File

@ -26,4 +26,9 @@ export const DEFAULT_VALUES = {
POSTHOG_KEY: "phc_YACSR3QpnlRHkbJx7t86aVp3LDAwzkgN82l7okHZEmR",
POSTHOG_HOST: "https://posthog.follow.is",
},
LOCAL: {
API_URL: "http://localhost:3000",
WEB_URL: "http://localhost:2233",
INBOXES_EMAIL: "@follow.re",
},
}

View File

@ -29,10 +29,16 @@ const envProfileMap = {
POSTHOG_KEY: DEFAULT_VALUES.STAGING.POSTHOG_KEY,
POSTHOG_HOST: DEFAULT_VALUES.STAGING.POSTHOG_HOST,
},
local: {
API_URL: DEFAULT_VALUES.LOCAL.API_URL,
WEB_URL: DEFAULT_VALUES.LOCAL.WEB_URL,
INBOXES_EMAIL: DEFAULT_VALUES.LOCAL.INBOXES_EMAIL,
},
}
export const getEnvProfiles__dangerously = () => envProfileMap
export type { envProfileMap }
/**
* @deprecated
* @description this env always use prod env, please use `proxyEnv` to access dynamic env
*/
export const env = {

View File

@ -9718,7 +9718,9 @@ declare const auth: {
appName: string;
database: (options: BetterAuthOptions) => better_auth.Adapter;
advanced: {
generateId: false;
database: {
generateId: false;
};
defaultCookieAttributes: {
sameSite: "none";
secure: true;
@ -11025,6 +11027,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -11058,6 +11063,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -11458,7 +11466,9 @@ declare const auth: {
appName: string;
database: (options: BetterAuthOptions) => better_auth.Adapter;
advanced: {
generateId: false;
database: {
generateId: false;
};
defaultCookieAttributes: {
sameSite: "none";
secure: true;
@ -12765,6 +12775,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -12798,6 +12811,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -15727,6 +15743,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -15760,6 +15779,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -15825,7 +15847,9 @@ declare const auth: {
appName: string;
database: (options: BetterAuthOptions) => better_auth.Adapter;
advanced: {
generateId: false;
database: {
generateId: false;
};
defaultCookieAttributes: {
sameSite: "none";
secure: true;
@ -17132,6 +17156,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;
@ -17165,6 +17192,9 @@ declare const auth: {
image: string | null;
handle: string | null;
twoFactorEnabled: boolean | null;
socialLinks: Record<string, string> | null;
bio: string | null;
website: string | null;
};
session: {
id: string;

View File

@ -224,6 +224,9 @@ class Morph {
isMe: isMe ?? false,
emailVerified: data.emailVerified,
twoFactorEnabled: data.twoFactorEnabled,
bio: data.bio,
website: data.website,
socialLinks: data.socialLinks,
}
}
}

View File

@ -1,6 +1,11 @@
import type { UserSchema } from "@follow/database/schemas/types"
export interface UserProfileEditable {
email: string
name: string
handle: string
image: string
bio?: string
website?: string
socialLinks?: UserSchema["socialLinks"]
}