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:
parent
68e1c24119
commit
95616e2d6c
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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", {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -224,6 +224,9 @@ class Morph {
|
|||
isMe: isMe ?? false,
|
||||
emailVerified: data.emailVerified,
|
||||
twoFactorEnabled: data.twoFactorEnabled,
|
||||
bio: data.bio,
|
||||
website: data.website,
|
||||
socialLinks: data.socialLinks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue