Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Avendaosander/Plataforma-social/llms.txt

Use this file to discover all available pages before exploring further.

Every authenticated user has a profile page at /home/my-profile that surfaces their avatar, stats, and published components. From the same page users can open the EditProfile modal to update their photo, username, and bio. A dedicated settings page at /home/my-profile/settings provides account management controls including password changes, account deletion, privacy toggles, and granular notification preferences — all backed by the Setting model in the database.

My Profile Page (/home/my-profile)

The profile page is composed of two main components: InfoProfile (static display) and EditProfile (modal overlay). The page queries the GraphQL API for the current user’s data using GET_USER with the session’s user.id as the variable:
// frontend/app/home/my-profile/page.tsx
function MyProfile() {
  const [isEditMode, setIsEditMode] = useState(false)
  const { data: session, status } = useSession()
  const { loading, error, data } = useQuery(GET_USER, {
    variables: {
      id: session?.user.id
    }
  })

  const handleEditMode = (state: boolean) => {
    setIsEditMode(state)
  }

  return (
    <>
      <section className='flex gap-5'>
        <InfoProfile handleEditMode={handleEditMode} user={data?.getUser}/>
      </section>
      <Badge />
      <section className='flex flex-col gap-5 w-full items-center'>
        <CardPost />
        <CardPost />
      </section>
      {isEditMode && (
        <div className="absolute top-0 bottom-0 left-0 right-0 bg-black/50 flex justify-center items-center">
          <EditProfile onClose={handleEditMode} user={data?.getUser}/>
        </div>
      )}
      <Button
        className='fixed bottom-5 right-5 px-3'
        startContent={<PlusIcon />}
      >
        Crear
      </Button>
    </>
  )
}

InfoProfile Component

InfoProfile renders the user’s public-facing information. The avatar falls back to a placeholder image when the avatar field is an empty string:
// frontend/app/components/profile/InfoProfile.tsx
<Image
  src={user?.avatar !== "" ? user?.avatar : "/LogoUVM.jpg"}
  alt='Avatar'
  width={120}
  height={120}
  className='aspect-square size-[120px]'
/>
Below the avatar, the component shows:

Username

Displayed as an <h2> alongside Editar Perfil (opens EditProfile modal), a share button (ShareRightIcon), and an options button (DotsIcon).

Stats Row

Three counters side by side: Seguidores (followers), Seguidos (following), and Componentes (total published components).

Description

Free-text bio paragraph (max-w-[60ch]) from the description field, displayed as-is with no truncation on the profile page.

Profile Tabs (Badge)

Below InfoProfile the Badge component renders two tab-style buttons: Componentes (active by default, highlighted with a top border) and Guardados (bookmarked posts). These filter which posts are listed beneath the profile header.

Editing Your Profile

Clicking Editar Perfil sets isEditMode to true, which renders EditProfile inside a full-screen dark overlay. The modal can be dismissed via the × button in the header or the Cancelar button in the footer.

EditProfile Fields

The file input (type="file", id="avatar") is hidden and triggered via its <label>. When a file is selected, URL.createObjectURL generates a local preview URL rendered immediately in the <Image> component — no upload occurs until the form is saved.
// frontend/app/components/profile/EditProfile.tsx
const handleFile = (e: ChangeEvent<HTMLInputElement>) => {
  const files = e.target.files;
  if (!files || files.length === 0) return;

  const selectFile = files[0];
  setDataImg(selectFile);
  setPreview(URL.createObjectURL(selectFile));
}

const avatarSrc = preview || user?.avatar || Logo?.src;
A standard text <Input> pre-populated with the current username. The value must be unique across the platform — the server returns an error if the chosen name is already taken.
A <textarea> with a live character counter. When the character count exceeds 250 the textarea ring turns red (ring-2 ring-maroon-800) and the counter label turns red as well, providing immediate visual feedback without blocking submission until the mutation enforces the limit.
// frontend/app/components/profile/EditProfile.tsx
let limitClasses = {
  textArea: dataEdit?.description.length > 250
    ? "ring-2 ring-maroon-800"
    : "ring-seagreen-950",
  span: dataEdit?.description.length > 250
    ? "text-maroon-900 font-normal"
    : "font-light"
}

// Counter display:
<span className={`${limitClasses.span} text-sm`}>
  {`${dataEdit?.description.length}/250`}
</span>

PUT_USER GraphQL Mutation

Clicking Guardar in the EditProfile footer fires the PUT_USER mutation with the updated fields:
# frontend/app/lib/graphql/users.ts
mutation PutUser($putUserId: String!, $username: String, $description: String, $avatar: String) {
  putUser(id: $putUserId, username: $username, description: $description, avatar: $avatar) {
    id
    username
    email
    password
    description
    avatar
  }
}
The handleSubmit function in EditProfile calls the mutation with the three editable fields:
// frontend/app/components/profile/EditProfile.tsx
const handleSubmit = () => {
  putUser({
    variables: {
      avatar: dataEdit?.avatar,
      username: dataEdit?.username,
      description: dataEdit?.description
    }
  })
}
The $putUserId variable is declared as required (String!) in the PUT_USER mutation, but the current handleSubmit implementation does not pass it in the variables object. The id value is available on dataEdit.id (populated from the user prop). This means the server will receive the mutation without an id, which will cause a GraphQL validation error at runtime.
The three update fields (username, description, avatar) are optional — only fields whose values have changed need to be sent. The mutation returns the full updated user object including the hashed password field.

Following System

Users can follow and unfollow each other from two surfaces: the inline Seguir / Siguiendo button on every CardPost card in the feed, and profile pages. Follow state is managed by a Follower join table in the database:
// server/prisma/schema.prisma
model Follower {
  follower    User     @relation("UserFollowers", fields: [idFollower], references: [id])
  idFollower  String

  following   User     @relation("UserFollowing", fields: [idFollowing], references: [id])
  idFollowing String

  @@id([idFollower, idFollowing])
}
The CardPost component toggles between Seguir (outline style) and Siguiendo (solid style) using local state:
// frontend/app/components/ui/CardPost.tsx
const [isFollowing, setIsFollowing] = useState(false)
const handleFollowing = () => {
  setIsFollowing(!isFollowing)
}

// Rendered conditionally:
{isFollowing ? (
  <Button
    variant='solid'
    color='primary'
    shape='md'
    size='md'
    className='px-3 py-0.5'
    onClick={handleFollowing}
  >
    Siguiendo
  </Button>
) : (
  <Button
    variant='outline'
    color='primary'
    shape='md'
    size='md'
    className='px-3 py-0.5 dark:bg-inherit dark:ring-white dark:text-white'
    onClick={handleFollowing}
  >
    Seguir
  </Button>
)}
Follower and following counts are displayed prominently in InfoProfile as part of the stats row.

Settings Page (/home/my-profile/settings)

The settings page is accessed via the Configuraciones menu item in the Navbar’s “Mas” dropdown. The page is structured with a left sidebar of anchor links and a right content panel that scrolls through three sections. The Navbar is hidden on this route (pathname === '/home/my-profile/settings').
// frontend/app/home/my-profile/settings/page.tsx
const paths = {
  default:        '/home/my-profile/settings',
  administrar:    '/home/my-profile/settings#administrar-cuenta',
  privacidad:     '/home/my-profile/settings#privacidad',
  notificaciones: '/home/my-profile/settings#notificaciones',
}

Section 1 — Administrar cuenta

Contains two sub-sections:
  • Control de cuenta: a destructive Eliminar button to permanently delete the account.
  • Datos de la cuenta: a navigation arrow button to the Change Password flow.

Section 2 — Privacidad

Contains a Visibilidad sub-section with a single Cuenta privada toggle (SwitchButton) backed by the private boolean on the Setting model. When enabled, the user’s posts and profile are not visible to users who do not follow them.

Section 3 — Notificaciones

Notification preferences are split into Notificaciones de escritorio and Notificaciones al correo, each controlled by individual SwitchButton toggles that map directly to fields on the Setting model. Notificaciones de escritorio is further divided into two groups:
Sub-groupToggle labelSetting fieldTypeDefault
InteraccionesCalificacionesn_ratingsBooleantrue
InteraccionesComentariosn_commentsBooleantrue
InteraccionesNuevos seguidoresn_followersBooleantrue
Del sistemaComponentes popularesn_populatesBooleantrue
Notificaciones al correo has a single Interacciones group:
Toggle labelSetting fieldTypeDefault
Calificacionesn_email_ratingsBooleantrue
Comentariosn_email_commentsBooleantrue
Nuevos seguidoresn_email_followersBooleantrue
The full Setting model from the Prisma schema:
// server/prisma/schema.prisma
model Setting {
  idSettings        String  @id @default(uuid())
  user              User    @relation(fields: [idUser], references: [id], onDelete: Cascade)
  idUser            String  @unique
  private           Boolean @default(false)
  n_ratings         Boolean @default(true)
  n_comments        Boolean @default(true)
  n_followers       Boolean @default(true)
  n_populates       Boolean @default(true)
  n_email_ratings   Boolean @default(true)
  n_email_comments  Boolean @default(true)
  n_email_followers Boolean @default(true)
}
A Setting record is automatically created for every new user when the postUser mutation runs on the server. All notification flags default to true, so new users receive all notification types until they opt out in the settings page.

Build docs developers (and LLMs) love