Skip to content

KyLoc20/twitter-shadow

Repository files navigation

Twitter-Shadow

This is an exercise for React & Typescript by building a very simple Twitter.

There is no backend involved and all the data is stored locally.

This is a Next.js project bootstrapped with create-next-app.

Getting Started

Run the development server:

npm run dev
# or
yarn dev

Open http://localhost:3000 with your browser to see the result.

Tech Stack

Features

  • Timeline Page

    • includes the home page(/home) and the user page(/{username}), the home page displays all the tweets, the user page displays only the tweets of the user
    • redirect the index page(/) to the home page(/home)
  • Register & Login & Logout

    • unique username password without security
    • as options in the left-side AppBar of timeline pages(/)(/{username})
    • start with the home page(/) of a user named @tourist if a user hasn't logged in
    • keep the logged-in status in local session after a user successfully registers or logs in
    • transfer to her/his timeline page(/{username}) after a user successfully registers or logs in
    • transfer to the home page(/) of a user named @tourist after a user logs out
  • Tweets in the timeline page

    • each tweet owns a unique incremental integer ID
    • users can post text tweets
    • tweets are in a descending order
    • tweets can be edited only by the posters
    • tweets can be deleted only by the posters
    • transfer to the detail page(/tweet/{tweet ID}) after clicking a tweet
  • AppBar

    • appears in leftside of the home page(/home) and the user page(/{username})
    • users can post a tweet quickly by clicking the Tweet Button
    • users can go to the home page(/) by clicking the Home Navigation Button
    • users can go to her/his timeline page(/{username}) by clicking the Profile Navigation Button
  • Tweets in the detail page

How I Made it

This little project is basically out of these principles:

  • Readable Code Comes 1st
  • Reusable Code Comes 2nd

Now let's look through the practices of above principles.

Component, Component, Component

Component = (Props) => UI

Here are 3 types of Components in the project.

Type Role Dependencies
Generic Component Atom utility none / other Generic Component
Common Component Reusable business Generic Component
Page Component Usual business, Chores Generic & Common Component

Generic Component

As the most atomic Components, Generic Components serve as the reinforcement from UI lib.

You can find them in @/components/generic and @/ui.

A UI Library Should Help to Reduce the Mental Burden and the Possibility of Making Mistakes

Take the Component Textfield for example.

You can find the practices in some forms like @/components/modals/SignInCard/SignIn/PasswordForm.tsx.

type TPasswordForm = {
  username: string;
  onAfterSubmit: Function;
};
function PasswordForm(props: React.PropsWithChildren<TPasswordForm>) {
  return (
    <>
      <Textfield
        id="username-disabled-input"
        prompt="Phone, email, or username"
        defaultValue={props.username}
        disabled
      />
      <Textfield
        id="password-input"
        ref={passwordTextfieldRef}
        prompt="Password"
        secretly
        onChange={handleInputPassword}
      />
    </>
  );
}

Textfield in PasswordForm:

Textfield

What does Textfield do?

  • Its appearance and behavior are aligned with a UI lib refering to Material Design and mym-UI.
  • It provides a reusable input which owns HTML attributes such as ref and defaultValue.
  • It provides readable props such as secretly which means <input type="password" />.

Common Component

As the reusable business Components, Common Component are dumb waiting to receive props.

They have nothing to do with State or Store in spite of containing a fair amount of reusable business logic.

NOTE:
Why some dumb Components are not called Common Components?

Some dumb Components are so simple to be written that they serve as Widgwets around their parent Components Like Avatar.

Some Components are too specific in business to be reused across the Pages like PasswordForm.

You can use Common Components safely and update State or Store in their parent Components.

You can find them in @/components/common.

Take the Component TweetEditor for example.

You can find the practices in the scenes where users need to edit their tweets like @/components/timeline/MainContentCard/TweetEditorCard.

function TweetEditorCard() {
  const { state: userState, dispatch: userDispatch } = useContext(UserStore);
  const { state: tweetState, dispatch: tweetDispatch } = useContext(TweetStore);
  //use a ref to control the inner <textarea>
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const handleCreateTweet = () => {
    const elTextarea = textareaRef.current;
    //make sure input valid
    if (elTextarea == null || elTextarea.value === "") return;
    //use the ref to get value from the inner <textarea>
    const content = elTextarea.value;
    const user: Poster = {
      nickname: userState.nickname,
      username: userState.username,
      avatarUrl: userState.avatarUrl,
    };
    //make a POST request to create a Tweet
    API.Tweet.postCreateTweet({ content, user }).then((tid) => {
      //update the Store when successful
      const doCreateTweet: TweetActions = {
        type: TweetActionTypes.Create,
        payload: _genTweetInstance(tid, content, user),
      };
      tweetDispatch(doCreateTweet);
      //use the ref to control the inner <textarea>
      elTextarea.value = "";
    });
  };
  return (
    <TweetEditor
      ref={textareaRef}
      user={userState}
      submitButtonMetaText="Tweet"
      textareaId="main-tweet-editor-textarea"
      textareaPlaceholder="What's happening?"
      onSubmit={handleCreateTweet}
    />
  );
}

TweetEditor when creating tweets:

TweetEditor when creating tweets

TweetEditor when updating tweets:

TweetEditor when updating tweets

What does TweetEditor do?

  • It is a large compound Component which combines Page Components including Avatar and Tools and Generic Components including AutosizeTextarea and Button.
  • It is reusable across the Big Page Components TweetEditorCard from timeline which allows to write tweets in Timeline Page and EditorCard from modals which allows to post tweets quickly from the left-side AppBar or edit tweets' content in each Tweet.
  • It is dumb therefore you always mess with Store and API in its parent Components.

Page Component

As the logic elements of Pages, Page Components are "people" of the Page "nation".

They are everywhere and serve as all kinds of jobs.

You can find them in @/components/timeline and @/components/modals.

CONVENTION:
Why many Page Components are named as "xxCard"?

They are "smart" or "container" which dealing with API and Store.

I choose to name them all as "Card" instead of "Container"

CONVENTION:
What are Widgets for?

For a "Card" Page Component there are some small dumb Page Components and constant varibles like STYLE or ICON.

They are regarded as "Widgets" put aside the regarding Page Components like TopBannerCard's Widgets


Unstyled Component

Generic Components are useful "bricks" when building Page Components especially Unstyled Components such as Box.

If you need an Avatar, let's try it with inline CSS styles:

function SimpleAvatar(props: { url: string }) {
  return (
    <div
      style={{
        marginRight: "12px",
        width: "48px",
        height: "48px",
        borderRadius: "50%",
        background: `no-repeat url(${props.url})`,
        backgroundSize: "contain",
      }}
    ></div>
  );
}

It is a little tiring to write these styles but it's OK.

But you feel shity when you need a LARGER Avatar, so you seal this with Props adding some "sweet" short names for a relief:

type TSweetAvatar = {
  url: string;
  m?: string; //margin
  w?: number; //width
  h?: number; //height
};
function SweetAvatar(props: TSweetAvatar) {
  return (
    <div
      style={{
        margin: props.m,
        width: props.w ? `${props.w}px` : "48px",
        height: props.h ? `${props.h}px` : "48px",
        borderRadius: "50%",
        background: `no-repeat url(${props.url})`,
        backgroundSize: "contain",
      }}
    ></div>
  );
}
//require a larger width and height
const LargerAvatarCase = () => (
  <SweetAvatar url="xxx" m="0 12px 0 0" w={64} h={64}></SweetAvatar>
);

How about making a Generic Component supports all the CSS properties?

Here the Unstyled Component Box comes:

import Box from "@/components/generic/containers/Box";

const LargerAvatarUsingBoxCase = () => (
  <Box
    m="0 12px 0 0"
    w={64}
    h={64}
    borderRadius={"50%"}
    bg="no-repeat url(xxx)"
    bgSize="contain"
  ></Box>
);

No need to write an Avatar any more, just write styles into Props as you want.

You can refer to sxProps to check the supported CSS properties of Unstyled Components including Box, Button, Text.


Component Babysitter

Babysitter = (...args) => Component with some Props

Unstyled Components are powerful but not enough:

  • Cumbersome that components receive style object as props in JSX -> To seperate styles from JSX
  • Box with styles is not READABLE -> To name components individually or not
  • Need some custom props in different situations -> To provide REUSABLE custom components

Here the Component Babysitter genCustomBox and defineCustomBox.

genCustomBox

import { genCustomBox } from "@/components/generic/containers/Box";

const AvatarUsingCustomBoxCase = () => {
  //a Container {display: flex; flex-direction: column;}
  const Wrapper = genCustomBox({ vertical: true }, {});
  const MyAvatar = genCustomBox(
    { noFlex: true }, //display: block;
    {
      w: 48,
      h: 48,
      p: "2px", //padding
      borderRadius: "50%",
      bg: `no-repeat url(xxx)`,
      bgSize: "contain",
    }
  );
  return (
    <Wrapper>
      <MyAvatar />
      <MyAvatar />
      <MyAvatar />
    </Wrapper>
  );
};

Now your Unstyled Components own their names Wrapper and MyAvatar which is easier to understand what they are for.

You use a TCustomBox object as the 1st argument.

-> To provide some short-cut settings like flex-direction or box-sizing

You use a sxProps object as the 2nd argument.

-> To provide CSS properties as you want

defineCustomBox

import {
  defineCustomBox,
  genCustomBox,
} from "@/components/generic/containers/Box";

const AvatarUsingDefinedBoxCase = () => {
  //provide Box with {display: block}
  const genBlockBox = defineCustomBox({ noFlex: true });
  const Wrapper = genBlockBox();
  const MyAvatar = genBlockBox({
    w: 48,
    h: 48,
    p: "2px", //padding
    borderRadius: "50%",
    bg: `no-repeat url(xxx)`,
    bgSize: "contain",
  });
  return (
    <Wrapper>
      <MyAvatar />
      <MyAvatar />
      <MyAvatar />
    </Wrapper>
  );
};

By default genCustomBox provides you with a display: flex, you have to specify for each if you don't want.

Now by using defineCustomBox you can customize your genCustomBox and using genCustomBox to customize your Box.

Wonderful!

Write a Button

import { genCustomButton } from "@/components/generic/Button";

const FollowButton = genCustomButton({
  variant: "plain",
  depressed: true,
  borderRadius: 18,
  rippleDisabled: true,
  height: 36,
  padding: "0 16px",
  backgroundColor: "rgb(15, 20, 25)",
  hoverBackgroundColor: "rgb(39, 44, 48)",
  inner: {
    color: "#fff",
    lineHeight: 18,
    fontSize: 15,
    fontWeight: 700,
    letterSpacing: "normal",
  },
});

function InteractionButtonGroup(props: TInteractionButtonGroup) {
  const Wrapper = genCustomBox();
  const handleSelect = () => props.onSelect("follow");
  return (
    <Wrapper>
      <FollowButton onClick={handleSelect}>Follow</FollowButton>
    </Wrapper>
  );
}
type TInteractionButtonGroup = {
  onSelect: (value: string) => void,
};

FollowButton in UserProfileCard:

FollowButton

Write a Button using Box

In spite of the given Unstyled Component Button, let's make a Button using genCustomBox which is based on Box.

import { genCustomBox } from "@/components/generic/containers/Box";

const NotifyButton = genCustomBox(
  {},
  {
    mr: "8px",
    mb: "12px",
    h: 34,
    w: 34,
    borderRadius: 9999,
    border: "1px solid rgb(207, 217, 222)",
    hoverBg: "rgba(15, 20, 25,0.1)",
    transition: "background 0.15s ease",
    cursor: "pointer",
    JC: "center",
    AI: "center",
  }
);

function InteractionButtonGroup(props: TInteractionButtonGroup) {
  const Wrapper = genCustomBox();
  const handleSelect = () => props.onSelect("notify");
  return (
    <Wrapper>
      <NotifyButton onClick={handleSelect}>
        <Icon svg={IconNotify} />
      </NotifyButton>
    </Wrapper>
  );
}
type TInteractionButtonGroup = {
  onSelect: (value: string) => void,
};

NotifyButton in UserProfileCard:

NotifyButton

About

To build a very simple Twitter with React and Typescript

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published