2024 06 20 user helping users use tui with localstorage react tutorials - bizkite-co/handterm GitHub Wiki
original file name: Codeium Chat - c0kw1RBCsRYTjY5JBcopU8VBladqdn6Z.md
summary: The user wanted to help users use a TUI by providing introductory text until they demonstrated some skills. They planned to use localStorage
to track progress and achievements non-linearly, needing a React-oriented approach, possibly with a custom hook.
I suggested creating a custom hook for tracking achievements, utilizing localStorage
, and updating achievements. For the TUI, I recommended mapping achievements to tutorial components and rendering tutorials based on achievements.
Later, the user intended to abstract tutorial components, passing variable values to a common TutorialComponent
. I proposed adjusting the HandTerm
component to manage tutorial state based on achievements and render the TutorialComponent
with the appropriate props.
date: 2024-06-20T21:24
Note: This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output.
User at 9:24 pm, Jun 20, 2024
I want to help users use my TUI by giving them some interoductory text until they deomonstrate some skills.
I'd like to use localStorage to register some levels of progress, and then update to local storage with levels of acheivement. The achievments might not be linear, so I can't just save the most recent achievement.
I want to make this in a very React oriented way, so it could involve a custom hook.
How can I organize and implement this?
Codeium at 9:25 pm, Jun 20, 2024
To organize and implement a system that tracks user progress and achievements in a TUI using React and localStorage, you can follow these steps:
-
Define the Achievements: Create a list of achievements that users can unlock. Each achievement should have a unique identifier.
-
Create a Custom Hook: Make a custom React hook that encapsulates the logic for tracking and updating achievements.
-
Use Local Storage: Utilize
localStorage
to persist the user's achievements across sessions. -
Update Achievements: Provide functions within the custom hook to update the achievements based on user actions.
Here's a simple implementation of a custom hook for tracking achievements:
import { useState, useEffect } from 'react';
// Define a type for the achievement
type Achievement = {
id: string;
name: string;
unlocked: boolean;
};
// Custom hook to manage achievements
function useAchievements(initialAchievements: Achievement[]) {
// State to hold the achievements
const [achievements, setAchievements] = useState<Achievement[]>([]);
// Load achievements from localStorage on component mount
useEffect(() => {
const storedAchievements = localStorage.getItem('achievements');
if (storedAchievements) {
setAchievements(JSON.parse(storedAchievements));
} else {
setAchievements(initialAchievements);
}
}, []);
// Save achievements to localStorage when they change
useEffect(() => {
localStorage.setItem('achievements', JSON.stringify(achievements));
}, [achievements]);
// Function to unlock an achievement
const unlockAchievement = (id: string) => {
setAchievements((prevAchievements) =>
prevAchievements.map((ach) =>
ach.id === id ? { ...ach, unlocked: true } : ach
)
);
};
return { achievements, unlockAchievement };
}
export default useAchievements;
Then, in your component, you can use this hook like so:
import React from 'react';
import useAchievements from './useAchievements';
const initialAchievements = [
{ id: 'first-command', name: 'First Command', unlocked: false },
{ id: 'first-game', name: 'First Game', unlocked: false },
// ... other achievements
];
const HandTerm = () => {
const { achievements, unlockAchievement } = useAchievements(initialAchievements);
// Call `unlockAchievement` whenever the user unlocks a new achievement
// For example, after a user types their first command:
// unlockAchievement('first-command');
// Render the TUI and any achievement-related UI here
return (
<div>
{/* Your TUI and other content */}
</div>
);
};
export default HandTerm;
This approach encapsulates the achievements logic within a custom hook, making it reusable and keeping your components clean. The achievements are saved to localStorage
, ensuring they persist across sessions. The unlockAchievement
function provided by the hook allows you to easily mark achievements as unlocked based on user actions within your application.
User at 10:11 am, Jun 21, 2024
That is a good place to start.
Now, in the HandTerm
, how should I provide a veriety of content based on the achievement
?
For instance, I have an asdf
achievement, and while that is not unlocked I would like to present an brief asdf
tutorial component. Then, when that achievement is unlocked but the jkl;
is not unlocked, I would like to present the jkl;
tutorial component.
What is the best way to organize that in React to accomidate many more tutorial achievements?
Codeium at 10:11 am, Jun 21, 2024
To organize and manage different tutorial components based on user achievements in React, you can use conditional rendering alongside the custom hook you've created for achievements. Here’s how you can structure your HandTerm
component to render the appropriate tutorial content:
-
Define Tutorial Components: Create a separate React component for each tutorial that you want to present to the user.
-
Map Achievements to Tutorials: Create a mapping of achievement IDs to their corresponding tutorial components.
-
Render Based on Achievements: Use the achievements from the custom hook to determine which tutorial component to render.
Here's what this might look like in practice:
import React from 'react';
import useAchievements from './useAchievements';
import AsdfTutorial from './tutorials/AsdfTutorial';
import JklTutorial from './tutorials/JklTutorial';
const initialAchievements = [
{ id: 'asdf', name: 'ASDF Tutorial', unlocked: false },
{ id: 'jkl;', name: 'JKL; Tutorial', unlocked: false },
// ... other achievements
];
const tutorials = {
'asdf': AsdfTutorial,
'jkl;': JklTutorial,
// ... other tutorial components
};
const HandTerm = () => {
const { achievements, unlockAchievement } = useAchievements(initialAchievements);
// Determine which tutorial to show based on achievements
const currentTutorialId = Object.keys(tutorials).find(
(tutorialId) => !achievements.find((ach) => ach.id === tutorialId && ach.unlocked)
);
const TutorialComponent = tutorials[currentTutorialId];
return (
<div>
{TutorialComponent && <TutorialComponent unlockAchievement={unlockAchievement} />}
{/* Rest of your TUI */}
</div>
);
};
export default HandTerm;
In this setup, you:
- Define a
tutorials
object that maps achievement IDs to their corresponding tutorial components. - Determine the current tutorial by finding the first tutorial whose corresponding achievement has not been unlocked.
- Conditionally render the
TutorialComponent
if it exists, passingunlockAchievement
as a prop so the tutorial can unlock the achievement when completed.
This approach provides a clear separation between the tutorial content and the logic for tracking achievements. It also scales well as you can simply add new tutorial components and their corresponding achievements to the tutorials
object and initialAchievements
array as needed.
User at 11:50 am, Jun 21, 2024
My HandTerm
is till a class component. I haven't been able to convert it to a functional component yet.
Codeium at 11:50 am, Jun 21, 2024
Given that HandTerm
is a class component, you can adapt the previous solution accordingly. Since hooks can't be used directly in class components, you'll need to manage the state and lifecycle methods manually. Here's how you can implement the behavior for showing tutorials based on achievements:
- Define tutorial components as before.
- Create a method in
HandTerm
to check achievements and determine which tutorial to show. - Use the component's state to track which tutorial is currently active.
- Update the achievement state in
localStorage
and the component's state when an achievement is unlocked.
Here's an example of how you could implement this in HandTerm
:
import React from 'react';
import AsdfTutorial from './tutorials/AsdfTutorial';
import JklTutorial from './tutorials/JklTutorial';
// Define a mapping from achievement IDs to tutorial components
const tutorialComponents = {
asdf: AsdfTutorial,
'jkl;': JklTutorial,
// ... other mappings
};
class HandTerm extends React.Component {
constructor(props) {
super(props);
this.state = {
achievements: this.loadAchievements(),
currentTutorialId: null,
};
}
componentDidMount() {
this.setState({
currentTutorialId: this.determineCurrentTutorial(),
});
}
loadAchievements() {
const storedAchievements = localStorage.getItem('achievements');
return storedAchievements ? JSON.parse(storedAchievements) : [];
}
saveAchievements(achievements) {
localStorage.setItem('achievements', JSON.stringify(achievements));
}
determineCurrentTutorial() {
const { achievements } = this.state;
for (const [id, Component] of Object.entries(tutorialComponents)) {
const achievement = achievements.find((ach) => ach.id === id);
if (!achievement || !achievement.unlocked) {
return id;
}
}
return null; // No more tutorials to show
}
unlockAchievement = (id) => {
this.setState((prevState) => {
const updatedAchievements = prevState.achievements.map((ach) =>
ach.id === id ? { ...ach, unlocked: true } : ach
);
// Save the updated achievements to localStorage
this.saveAchievements(updatedAchievements);
return {
achievements: updatedAchievements,
currentTutorialId: this.determineCurrentTutorial(),
};
});
};
renderTutorial() {
const TutorialComponent = tutorialComponents[this.state.currentTutorialId];
if (TutorialComponent) {
return <TutorialComponent unlockAchievement={this.unlockAchievement} />;
}
return null;
}
render() {
return (
<div>
{this.renderTutorial()}
{/* Rest of your TUI */}
</div>
);
}
}
export default HandTerm;
In this HandTerm
class component:
- We load the achievements from
localStorage
in theloadAchievements
method and initialize the state with them in the constructor. - We determine the current tutorial based on the achievements in
determineCurrentTutorial
. - We unlock an achievement and update both the local state and
localStorage
inunlockAchievement
. - We conditionally render the current tutorial in the
renderTutorial
method.
This approach allows you to manage the tutorial state within the HandTerm
class component while still keeping the tutorial logic separate from the main TUI logic.
User at 11:58 am, Jun 21, 2024
Instead of having a different component for eacy Tutorial, I was intending to have this abstraction which I can pass in the variable values:
// TutorialComponent.tsx
import { ChordDisplay } from "./ChordDisplay";
import { Chord } from "../types/Types";
export interface ITutorialComponentProps {
unlockAchievement: (id: string) => {};
characters: Chord[];
prompt: string;
isInTutorial: boolean;
}
export function TutorialComponent(props: any) {
return (
<div
className="tutorial-component"
hidden={!props.isInTutorial}
>
<pre>{props.prompt}</pre>
<div className="chord-display-container">
{
props.characters.map((character: Chord, index: number) => {
return <ChordDisplay
key={index}
displayChar={character.key}
displayCharCode={character.chordCode}
/>
})
}
</div>
</div>
);
}
Codeium at 11:58 am, Jun 21, 2024
Given that you have a common TutorialComponent
that you'd like to use for different tutorials by passing variable values, you'll want to adjust the HandTerm
class component to manage the state of which tutorial to show based on the current achievement, and then render the TutorialComponent
with the appropriate props.
Here's how you could update the HandTerm
component to work with this approach:
import React from 'react';
import { TutorialComponent } from './TutorialComponent';
import { Chord } from '../types/Types';
// Define your tutorials as data
const tutorials = [
{
id: 'asdf',
prompt: 'Type `asdf` to unlock',
characters: [/*...Chord objects for asdf...*/],
unlocked: false,
},
{
id: 'jkl;',
prompt: 'Type `jkl;` to unlock',
characters: [/*...Chord objects for jkl;...*/],
unlocked: false,
},
// ... other tutorials
];
class HandTerm extends React.Component {
constructor(props) {
super(props);
this.state = {
achievements: this.loadAchievements(),
currentTutorial: null,
};
}
componentDidMount() {
this.setState({
currentTutorial: this.determineCurrentTutorial(),
});
}
loadAchievements() {
const storedAchievements = localStorage.getItem('achievements');
return storedAchievements ? JSON.parse(storedAchievements) : tutorials;
}
saveAchievements(achievements) {
localStorage.setItem('achievements', JSON.stringify(achievements));
}
determineCurrentTutorial() {
const { achievements } = this.state;
return achievements.find((tutorial) => !tutorial.unlocked) || null;
}
unlockAchievement = (id) => {
this.setState((prevState) => {
const updatedAchievements = prevState.achievements.map((tutorial) =>
tutorial.id === id ? { ...tutorial, unlocked: true } : tutorial
);
this.saveAchievements(updatedAchievements);
return {
achievements: updatedAchievements,
currentTutorial: this.determineCurrentTutorial(),
};
});
};
render() {
const { currentTutorial } = this.state;
return (
<div>
{currentTutorial && (
<TutorialComponent
unlockAchievement={this.unlockAchievement}
characters={currentTutorial.characters}
prompt={currentTutorial.prompt}
isInTutorial={true}
/>
)}
{/* Rest of your TUI */}
</div>
);
}
}
export default HandTerm;
In this updated HandTerm
component:
- Tutorials are defined as an array of objects with
id
,prompt
,characters
, andunlocked
properties. - The
loadAchievements
method initializes the achievements state with the tutorials data if it's not found inlocalStorage
. - The
determineCurrentTutorial
method finds the first tutorial that has not been unlocked. - The
unlockAchievement
method is updated to modify the tutorial'sunlocked
state and the current tutorial. - The
render
method conditionally includesTutorialComponent
with the props from the current tutorial, if there is one to show.
This approach allows you to manage multiple tutorials with a single component, using the state to track which tutorial's data to pass to TutorialComponent
.