Goal

Add cookie helpers, an auth slice that stores the user in localStorage and the token in a cookie, and a Tailwind LoginForm shell (API call comes next).

1. Create src/utils/cookies.js

export function getCookie(name) {
  const match = document.cookie
    .split("; ")
    .find((row) => row.startsWith(`${name}=`));
  if (!match) return null;
  return decodeURIComponent(match.split("=").slice(1).join("="));
}

export function setCookie(name, value, days = 7) {
  const maxAge = days * 24 * 60 * 60;
  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
}

export function deleteCookie(name) {
  document.cookie = `${encodeURIComponent(name)}=; path=/; max-age=0; SameSite=Lax`;
}

Chunks explained

Chunk What it does
getCookie Reads one cookie by name
setCookie Writes cookie with max-age (expires after days)
SameSite=Lax Reasonable default against cross-site POST CSRF
deleteCookie Expires the cookie immediately
encodeURIComponent Safe for tokens / special characters

2. Create src/store/authSlice.js

import { createSlice } from "@reduxjs/toolkit";
import { deleteCookie, getCookie, setCookie } from "../utils/cookies";

const TOKEN_KEY = "hireboard-token";
const USER_KEY = "hireboard-user";

function readStoredUser() {
  try {
    const raw = localStorage.getItem(USER_KEY);
    return raw ? JSON.parse(raw) : null;
  } catch {
    return null;
  }
}

const authSlice = createSlice({
  name: "auth",
  initialState: {
    user: readStoredUser(),
    token: getCookie(TOKEN_KEY),
  },
  reducers: {
    setCredentials(state, action) {
      const payload = action.payload;
      state.token = payload.accessToken;
      state.user = {
        id: payload.id,
        username: payload.username,
        email: payload.email,
        firstName: payload.firstName,
        lastName: payload.lastName,
        image: payload.image,
      };

      setCookie(TOKEN_KEY, payload.accessToken, 7);
      localStorage.setItem(USER_KEY, JSON.stringify(state.user));
    },
    logout(state) {
      state.user = null;
      state.token = null;
      deleteCookie(TOKEN_KEY);
      localStorage.removeItem(USER_KEY);
    },
  },
});

export const { setCredentials, logout } = authSlice.actions;
export default authSlice.reducer;

Chunks explained

Chunk What it does
token from cookie Restored with getCookie on load
user from localStorage Restored with JSON.parse on load
setCredentials Writes both stores after a successful login
logout Clears Redux + cookie + localStorage
Selectors Reusable auth checks for routes later

Storage map

Field Storage Key
Access token Cookie hireboard-token
User object localStorage hireboard-user

3. Register the slice in src/store/index.js

import { configureStore } from "@reduxjs/toolkit";
import uiReducer from "./uiSlice";
import authReducer from "./authSlice";

export const store = configureStore({
  reducer: {
    ui: uiReducer,
    auth: authReducer,
  },
});

State shape:

{
  ui: { theme },
  auth: { user, token },
}

4. LoginForm.jsx

Create src/components/LoginForm.jsx:

import { useState } from "react";

function LoginForm() {
  const [username, setUsername] = useState("emilys");
  const [password, setPassword] = useState("emilyspass");

  function handleSubmit(event) {
    event.preventDefault();
    // RTK Query login mutation is added in the next lesson
    console.log("Login will call API with:", { username, password });
  }

  return (
    <div className="mx-auto max-w-md rounded-xl border border-slate-200 bg-card p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900">
      <h2 className="text-2xl font-semibold text-ink dark:text-white">
        Sign in to HireBoard
      </h2>
      <p className="mt-2 text-sm text-muted dark:text-slate-400">
        Use the DummyJSON demo account pre-filled below.
      </p>

      <form onSubmit={handleSubmit} className="mt-6 space-y-4">
        <div>
          <label
            htmlFor="username"
            className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
          >
            Username
          </label>
          <input
            id="username"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm outline-none ring-brand-500 focus:ring-2 dark:border-slate-600 dark:bg-slate-950"
            autoComplete="username"
          />
        </div>

        <div>
          <label
            htmlFor="password"
            className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
          >
            Password
          </label>
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm outline-none ring-brand-500 focus:ring-2 dark:border-slate-600 dark:bg-slate-950"
            autoComplete="current-password"
          />
        </div>

        <button
          type="submit"
          className="w-full rounded-md bg-brand-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-700"
        >
          Sign in
        </button>
      </form>
    </div>
  );
}

export default LoginForm;

5. Show the form in App.jsx for now

import { useEffect } from "react";
import { useSelector } from "react-redux";
import Navbar from "./components/Navbar";
import LoginForm from "./components/LoginForm";

function App() {
  const theme = useSelector((state) => state.ui.theme);
  const token = useSelector((state) => state.auth.token);

  useEffect(() => {
    document.documentElement.classList.toggle("dark", theme === "dark");
  }, [theme]);

  return (
    <div className="min-h-screen bg-surface font-sans text-ink antialiased dark:bg-slate-950 dark:text-slate-100">
      <Navbar />
      <main className="mx-auto max-w-5xl px-6 py-10">
        {token ? (
          <p className="text-muted dark:text-slate-400">
            Signed in. Real routes arrive in the TanStack Router lesson.
          </p>
        ) : (
          <LoginForm />
        )}
      </main>
    </div>
  );
}

export default App;

Temporary gating only , TanStack Router will own this properly soon.

Check

  • DevTools → Application: no token cookie yet; after you manually test setCredentials later you will see the split.
  • Login form renders with demo credentials.
  • Submit logs credentials to the console.

Assessment: Taskly : Auth slice

Add cookie helpers and an auth slice. Store user in localStorage (taskly-user) and token in a cookie (taskly-token). Build a Tailwind LoginForm shell (API call comes next).

Done when: login form renders; slice has user, token, setCredentials, logout.

Next: real DummyJSON login via RTK Query mutation.