op-ent: open-source school management

After working on yNotes, an unofficial client for existing ENTs (French school management systems), it was finally time to create my own... but open-source!

op-ent logo
op-ent logo

This content is outdated. However, it’s not completely irrelevant as it was true in the past and helps understanding current situation of this project.

Introduction

op-ent is an open-source school management system, called ENT (which stands for “digital workspace” in French). It aims to provide basic features such as subjects, grades, calendar, and homework in its first phase. The goal is to make the platform more unique once it has reached feature parity with current competitors such as EcoleDirecte and Pronote. Some ideas for future features include:

  • Gamification (achievements)
  • A knowledge center
  • Anonymous reporting for issues such as harassment
  • A built-in editor similar to Notion

As the initiator of this project, I’m working on all aspects of development, including frontend and backend development, design (including design system and brand identity), communication, project management, and community management.

The project utilizes a lot of technologies, including:

There isn’t much to show at this time, but you can find the project on the GitHub organization and the website.

GitHub organization screenshot
GitHub organization screenshot

Purpose and Goal

I have contributed to and maintained a project called yNotes in the past (Read more). The goal of yNotes was to provide an unofficial client for the main school management systems (ENTs) and it was popular among students. Even though the project is no longer maintained, it reached almost 7,000 downloads on Android in December 2022, which represents a 250% increase over two months. This goes to show that yNotes (and now op-ent) are addressing a real issue: current ENTs are not very good.

My goal is fairly simple: I want to make op-ent the most used school management system in France. While it is still a work in progress, I am confident that this project has a lot of potential.

To achieve this ambitious goal, the project has been divided into several sub-projects, including:

I decided not to use a monorepo for this project because I am not very familiar with it, and I prefer the one-repository-per-project approach used in AdonisJS.

Spotlight: unstyled-ui

While there are many interesting aspects of op-ent to discuss, for the sake of this page, let’s focus on unstyled-ui. It is worth noting that I may write dedicated articles for other sub-projects in the future.

Useful links:

unstyled-ui is a React UI library that aims to solve the problem of mixing full-featured libraries like Material UI and headless components like Headless UI. You can find the full motivation for this library here. In short, it is configurable, customizable, and type-safe.

While developing this library, I encountered a few issues:

Typed polymorphic components in React

According to freeCodeCamp:

In the world of React components, a polymorphic component is a component that can be rendered with a different container element / node.

While polymorphic components are not necessarily difficult to implement, getting the types correct can be challenging. For example, consider a component called Button that should be polymorphic because it can sometimes be a link. Polymorphism in this case is controlled by the as prop, which specifies the element type (a string or a React component). When calling the component like this:

<Button as="a" href="/">
    Click!
</Button>

We want the IDE to provide autocompletion for the a element and not the button.

After doing some research and experimenting, I came up with the following solution (I apologize for not being able to quote the source):

// polymorphic.ts
import type React from 'react'

// Source: https://github.com/emotion-js/emotion/blob/master/packages/styled-base/types/helper.d.ts
// A more precise version of just React.ComponentPropsWithoutRef on its own
export type PropsOf<
    C extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<unknown>
> = JSX.LibraryManagedAttributes<C, React.ComponentPropsWithoutRef<C>>

type AsProp<C extends React.ElementType> = {
    /**
     * An override of the default HTML tag.
     * Can also be another React component.
     */
    as?: C
}

/**
 * Allows for extending a set of props (`ExtendedProps`) by an overriding set of props
 * (`OverrideProps`), ensuring that any duplicates are overridden by the overriding
 * set of props.
 */
export type ExtendableProps<
    ExtendedProps = Record<string, unknown>,
    OverrideProps = Record<string, unknown>
> = OverrideProps & Omit<ExtendedProps, keyof OverrideProps>

/**
 * Allows for inheriting the props from the specified element type so that
 * props like children, className & style work, as well as element-specific
 * attributes like aria roles. The component (`C`) must be passed in.
 */
export type InheritableElementProps<
    C extends React.ElementType,
    Props = Record<string, unknown>
> = ExtendableProps<PropsOf<C>, Props>

/**
 * A more sophisticated version of `InheritableElementProps` where
 * the passed in `as` prop will determine which props can be included
 */
export type PolymorphicComponentProps<
    C extends React.ElementType,
    Props = Record<string, unknown>
> = InheritableElementProps<C, Props & AsProp<C>>

/**
 * Utility type to extract the `ref` prop from a polymorphic component
 */
export type PolymorphicRef<C extends React.ElementType> =
    React.ComponentPropsWithRef<C>['ref']
/**
 * A wrapper of `PolymorphicComponentProps` that also includes the `ref`
 * prop for the polymorphic component
 */

export type PolymorphicComponentPropsWithRef<
    C extends React.ElementType,
    Props = Record<string, unknown>
> = PolymorphicComponentProps<C, Props> & { ref?: PolymorphicRef<C> }

export type ComponentProps<
    C extends React.ElementType,
    P
> = PolymorphicComponentPropsWithRef<C, P>
// Button.tsx
import React, { forwardRef } from 'react'
import type { ComponentProps, PolymorphicRef } from './polymorphic'

export type ButtonProps = {
    as?: React.ElementType
    children?: React.ReactNode
}

export type ButtonComponent = <C extends React.ElementType = 'button'>(
    props: ComponentProps<C, ButtonProps>
) => React.ReactElement | null

export const Button: ButtonComponent = forwardRef(
    <C extends React.ElementType = 'button'>(
        props: ComponentProps<C, ButtonProps>,
        ref?: PolymorphicRef<C>
    ) => {
        const { as = 'button', children, ...rest } = props
        const Component = as as React.ElementType

        return (
            <Component ref={ref} {...rest}>
                {children}
            </Component>
        )
    }
)

TypeScript and module override

By default, unstyled-ui does not make many decisions about props and their typings, so I wanted to provide a flexible way for users of the library to customize and extend the current types. This idea was inspired by Mantine:

import { Tuple, DefaultMantineColor } from '@mantine/core'

type ExtendedCustomColors =
    | 'primaryColorName'
    | 'secondaryColorName'
    | DefaultMantineColor

declare module '@mantine/core' {
    export interface MantineThemeColorsOverride {
        colors: Record<ExtendedCustomColors, Tuple<string, 10>>
    }
}

However, I was not fully satisfied with how this worked internally, so I ended up modifying it and creating the following:

declare module '@op-ent/unstyled-ui' {
    interface CustomizableComponentsPropsOverride {
        button: {
            variant: 'primary' | 'secondary'
        }
    }
}

This allows users to create a correctly typed config using the public library API:

export const { config, extendConfig } = createConfig({
    components: {
        button: {
            defaultProps: {
                variant: 'secondary',
            },
            customProps: ['variant'],
        },
    },
})

Here is the code that makes this possible:

import type { DeepPartial } from 'ts-essentials'
import type {
    AccordionProps,
    ButtonProps,
    ButtonGroupProps,
    IconButtonProps,
} from '..'

export type ConfigOverride = DeepPartial<Config>

export type Config = {
    /**
     * Used in {identifierTemplate}
     */
    prefix: string
    /**
     * @default `${prefix}-${id}`
     */
    identifierTemplate: (ctx: { prefix: string; id: string }) => string
    /**
     * @default `data-${prefix}-${prop}`
     */
    dataPropTemplate: (ctx: { prefix: string; prop: string }) => string
    components: ComponentsConfig
}

// eslint-disable-next-line @typescript-eslint/ban-types
export type CustomProps<T extends {}> = (keyof T extends never
    ? string
    : keyof T)[]

export type ComponentName = keyof ComponentsConfig

export type ComponentsConfig = {
    accordion: {
        defaultProps: DeepPartial<AccordionProps>
        customProps: CustomProps<CustomizableComponentsProps['accordion']>
    }
    button: {
        defaultProps: DeepPartial<ButtonProps>
        customProps: CustomProps<CustomizableComponentsProps['button']>
    }
    buttonGroup: {
        defaultProps: DeepPartial<ButtonGroupProps>
        customProps: CustomProps<CustomizableComponentsProps['buttonGroup']>
    }
    iconButton: {
        defaultProps: DeepPartial<IconButtonProps>
        customProps: CustomProps<CustomizableComponentsProps['iconButton']>
    }
}

// eslint-disable-next-line @typescript-eslint/ban-types
export type DefaultCustomizableComponentsProps = Record<ComponentName, {}>

export type CustomizableComponentsPropsOverride = Record<string, never>

export type CustomizableComponentsProps = DefaultCustomizableComponentsProps &
    CustomizableComponentsPropsOverride

I am interested in exploring the following topics in the future:

  • Stabilize the internal API to facilitate contributions
  • Make unstyled-ui framework agnostic to enable sharing styles across UI frameworks
  • Allow customization of component rendering through config
  • Allow users to easily integrate custom components
  • Consider renaming @op-ent/unstyled-ui to a shorter and more memorable name, such as @uui/core and @uui/react.

Unstyled UI repository cover
Unstyled UI repository cover

Current status

op-ent is currently in active development and is not expected to reach an alpha or MVP stage until August 2023. Please note that this page may not always reflect the most recent changes to the project, as it is likely to evolve quickly.

Lessons Learned

Working on op-ent has provided many valuable lessons, including:

  • Project management using tools such as GitHub and Linear
  • Community management on platforms like GitHub (with issues, PRs i.e contributions) and Discord (Join us!)
  • Awesome new technologies
  • Experience in building and scaling a library

Overall, I am satisfied with the technologies I have chosen for the project, with the exception of certain components libraries like Chakra UI and Mantine. This motivated me to create unstyled-ui.

I am pleased with the progress of the project and believe it has the potential to be useful. If you are interested in contributing to the project or learning more, please feel free to reach out to me or check out the contributing guide.

Get in touch

Feel free to reach out if you're looking for a developer, have a question or just want to connect.