Compare commits
8 Commits
2abc6bfb38
...
8b0ddcecec
Author | SHA1 | Date | |
---|---|---|---|
|
8b0ddcecec | ||
|
1c48bb36d3 | ||
|
2bb0cc35f9 | ||
|
57ba0e3d49 | ||
|
579e7265fc | ||
|
0dac64f1e2 | ||
|
adc1bba9e0 | ||
|
c0b3648fd2 |
|
@ -6,6 +6,7 @@ use btleplug::{
|
||||||
};
|
};
|
||||||
pub use state::{CentralState, ChannelState, PeripheralItem};
|
pub use state::{CentralState, ChannelState, PeripheralItem};
|
||||||
use tauri::{async_runtime::RwLock, AppHandle, Emitter, State};
|
use tauri::{async_runtime::RwLock, AppHandle, Emitter, State};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bluetooth, errors,
|
bluetooth, errors,
|
||||||
|
@ -157,3 +158,31 @@ pub async fn save_pattern(
|
||||||
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_pattern(
|
||||||
|
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
|
pattern_id: Uuid,
|
||||||
|
) -> Result<Option<Pattern>, errors::AppError> {
|
||||||
|
let state = app_state.read().await;
|
||||||
|
let pattern = state
|
||||||
|
.db
|
||||||
|
.get_pattern(&pattern_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
||||||
|
Ok(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_pattern(
|
||||||
|
app_state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
|
pattern_id: Uuid,
|
||||||
|
) -> Result<(), errors::AppError> {
|
||||||
|
let state = app_state.read().await;
|
||||||
|
state
|
||||||
|
.db
|
||||||
|
.remove_pattern(&pattern_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| errors::AppError::StorageFailure(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -66,7 +66,9 @@ pub fn run() {
|
||||||
cmd::start_scan_devices,
|
cmd::start_scan_devices,
|
||||||
cmd::stop_scan_devices,
|
cmd::stop_scan_devices,
|
||||||
cmd::list_patterns,
|
cmd::list_patterns,
|
||||||
cmd::save_pattern
|
cmd::save_pattern,
|
||||||
|
cmd::get_pattern,
|
||||||
|
cmd::remove_pattern
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { atom } from 'jotai';
|
import { atom, useSetAtom } from 'jotai';
|
||||||
import { atomFamily, atomWithRefresh } from 'jotai/utils';
|
import { atomWithRefresh } from 'jotai/utils';
|
||||||
import { get, reduce } from 'lodash-es';
|
import { get, reduce } from 'lodash-es';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
|
||||||
|
|
||||||
export enum FrequencyShifting {
|
export enum FrequencyShifting {
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +92,7 @@ export class Pattern {
|
||||||
this.createdAt = dayjs().valueOf();
|
this.createdAt = dayjs().valueOf();
|
||||||
this.lastModifiedAt = null;
|
this.lastModifiedAt = null;
|
||||||
this.smoothRepeat = true;
|
this.smoothRepeat = true;
|
||||||
this.Pulses = [];
|
this.pulses = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,18 +121,31 @@ export function totalDuration(pattern: Pattern): number {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PatternsAtom = atomFamily((keyword: string) =>
|
export const SearchKeywordAtom = atom<string | null>(null);
|
||||||
atomWithRefresh(async () => {
|
export const PatternsAtom = atomWithRefresh(async (get) => {
|
||||||
try {
|
try {
|
||||||
const patterns = await invoke<Pattern[]>('list_patterns', { keyword });
|
const keyword = get(SearchKeywordAtom);
|
||||||
return patterns;
|
const patterns = await invoke<Pattern[]>('list_patterns', { keyword });
|
||||||
} catch (e) {
|
return patterns;
|
||||||
console.error('[retrieving pattern list]', e);
|
} catch (e) {
|
||||||
|
console.error('[retrieving pattern list]', e);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
export const SelectedPatternIdAtom = atom<string | null>(null);
|
||||||
|
export const CurrentPatternAtom = atom<Pattern | null>(async (get) => {
|
||||||
|
try {
|
||||||
|
const patternId = get(SelectedPatternIdAtom);
|
||||||
|
if (patternId === null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return [];
|
const pattern = await invoke('get_pattern', { patternId });
|
||||||
}),
|
return pattern;
|
||||||
);
|
} catch (e) {
|
||||||
export const CurrentPatternAtom = atom<Pattern | null>(null);
|
console.error('[retrieving pattern]', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
export const PulsesInCurrentPatternAtom = atom(
|
export const PulsesInCurrentPatternAtom = atom(
|
||||||
(get) => get(CurrentPatternAtom)?.pulses ?? [],
|
(get) => get(CurrentPatternAtom)?.pulses ?? [],
|
||||||
(get, set, pulse: Pulse) => {
|
(get, set, pulse: Pulse) => {
|
||||||
|
@ -155,3 +169,27 @@ export const CurrentPatternDuration = atom((get) => {
|
||||||
if (!currentPattern) return 0;
|
if (!currentPattern) return 0;
|
||||||
return totalDuration(currentPattern);
|
return totalDuration(currentPattern);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useSavePattern() {
|
||||||
|
const refreshPatterns = useSetAtom(PatternsAtom);
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
|
||||||
|
const savePattern = async (pattern: Pattern) => {
|
||||||
|
try {
|
||||||
|
await invoke('save_pattern', { pattern });
|
||||||
|
refreshPatterns();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[save pattern]', error);
|
||||||
|
showToast(
|
||||||
|
NotificationType.ERROR,
|
||||||
|
'Failed to save pattern. Please try again.',
|
||||||
|
'material-symbols-light:error-outline',
|
||||||
|
ToastDuration.MEDIUM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return savePattern;
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<Route path="/pattern-editor">
|
<Route path="/pattern-editor">
|
||||||
<Route index element={<PatternNavigator />} />
|
<Route index element={<PatternNavigator />} />
|
||||||
<Route path="new" element={<CreatePattern />} />
|
<Route path="new" element={<CreatePattern />} />
|
||||||
<Route path=":pattern" element={<PatternEditor />} />
|
<Route path="edit" element={<PatternEditor />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -1,14 +1,30 @@
|
||||||
import { Icon } from '@iconify/react/dist/iconify.js';
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
||||||
import { FC } from 'react';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { FC, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import EditableContent from '../../components/EditableContent';
|
import EditableContent from '../../components/EditableContent';
|
||||||
|
import { CurrentPatternAtom, SelectedPatternIdAtom } from '../../context/Patterns';
|
||||||
import styles from './PatternHeader.module.css';
|
import styles from './PatternHeader.module.css';
|
||||||
|
|
||||||
const PatternHeader: FC = () => {
|
const PatternHeader: FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const currentPattern = useAtomValue(CurrentPatternAtom);
|
||||||
|
const setActivePattern = useSetAtom(SelectedPatternIdAtom);
|
||||||
|
const handleClosePattern = useCallback(() => {
|
||||||
|
setActivePattern(null);
|
||||||
|
navigate('/library');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pattern_header}>
|
<div className={styles.pattern_header}>
|
||||||
<EditableContent as="h3" placeholder="Pattern Name" additionalClassName={styles.content} />
|
<EditableContent
|
||||||
|
as="h3"
|
||||||
|
placeholder="Pattern Name"
|
||||||
|
value={currentPattern?.name ?? null}
|
||||||
|
additionalClassName={styles.content}
|
||||||
|
/>
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
<button className="tonal">
|
<button className="tonal" onClick={handleClosePattern}>
|
||||||
<Icon icon="material-symbols-light:close" />
|
<Icon icon="material-symbols-light:close" />
|
||||||
Close Edit
|
Close Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -3,5 +3,27 @@
|
||||||
flex: 2;
|
flex: 2;
|
||||||
border-radius: calc(var(--border-radius) * 2);
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
background-color: var(--color-surface-container);
|
background-color: var(--color-surface-container);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.empty_promption {
|
||||||
|
flex: 2;
|
||||||
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
|
background-color: var(--color-surface-container);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
.promption {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.spacer {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,27 @@
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
import { CurrentPatternAtom, Pattern } from '../../context/Patterns';
|
||||||
import styles from './PatternDetail.module.css';
|
import styles from './PatternDetail.module.css';
|
||||||
|
|
||||||
const PatternDetail: FC = () => {
|
const EmptyPromption: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty_promption}>
|
||||||
|
<div className={styles.promption}>
|
||||||
|
<span className="empty_prompt">Select a pattern from left first.</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.spacer} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Detail: FC<{ pattern: Pattern }> = ({ pattern }) => {
|
||||||
return <div className={styles.pattern_detail}></div>;
|
return <div className={styles.pattern_detail}></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PatternDetail: FC = () => {
|
||||||
|
const currentPattern = useAtomValue(CurrentPatternAtom);
|
||||||
|
|
||||||
|
return !currentPattern ? <EmptyPromption /> : <Detail pattern={currentPattern} />;
|
||||||
|
};
|
||||||
|
|
||||||
export default PatternDetail;
|
export default PatternDetail;
|
||||||
|
|
|
@ -24,4 +24,28 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: calc(var(--spacing) * 2);
|
gap: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.pattern_card {
|
||||||
|
padding: calc(var(--spacing) * 3) calc(var(--spacing) * 2);
|
||||||
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--spacing) * 2);
|
||||||
|
box-shadow: var(--elevation-0);
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in oklch, var(--color-on-surface) 8%, transparent);
|
||||||
|
box-shadow: var(--elevation-2-ambient), var(--elevation-2-umbra);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: color-mix(in oklch, var(--color-on-surface) 18%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,59 @@
|
||||||
import { Icon } from '@iconify/react/dist/iconify.js';
|
import { Icon } from '@iconify/react/dist/iconify.js';
|
||||||
import cx from 'clsx';
|
import cx from 'clsx';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDebounce } from 'react-use';
|
import { useDebounce } from 'react-use';
|
||||||
import { ScrollArea } from '../../components/ScrollArea';
|
import { ScrollArea } from '../../components/ScrollArea';
|
||||||
import { PatternsAtom } from '../../context/Patterns';
|
import {
|
||||||
|
Pattern,
|
||||||
|
PatternsAtom,
|
||||||
|
SearchKeywordAtom,
|
||||||
|
SelectedPatternIdAtom,
|
||||||
|
totalDuration,
|
||||||
|
} from '../../context/Patterns';
|
||||||
import styles from './Patterns.module.css';
|
import styles from './Patterns.module.css';
|
||||||
|
|
||||||
|
const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => {
|
||||||
|
const [selectedId, setSelectedId] = useAtom(SelectedPatternIdAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const duration = useMemo(() => {
|
||||||
|
return totalDuration(pattern) / 1000;
|
||||||
|
}, [pattern]);
|
||||||
|
const selected = useMemo(() => selectedId === pattern.id, [selectedId, pattern]);
|
||||||
|
const handleSingleClick = useCallback(() => {
|
||||||
|
if (selectedId === pattern.id) {
|
||||||
|
setSelectedId(null);
|
||||||
|
} else {
|
||||||
|
setSelectedId(pattern.id);
|
||||||
|
}
|
||||||
|
}, [pattern, selectedId]);
|
||||||
|
const handleDblClick = useCallback(() => {
|
||||||
|
setSelectedId(pattern.id);
|
||||||
|
navigate('/pattern-editor/edit');
|
||||||
|
}, [pattern]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(styles.pattern_card, selected && styles.selected)}
|
||||||
|
onClick={handleSingleClick}
|
||||||
|
onDoubleClick={handleDblClick}>
|
||||||
|
<h5 className={styles.name}>{pattern.name}</h5>
|
||||||
|
<span>{duration.toFixed(2)} s</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Patterns: FC = () => {
|
const Patterns: FC = () => {
|
||||||
const [rawKeyword, setRawKeyword] = useState<string>('');
|
const [rawKeyword, setRawKeyword] = useState<string>('');
|
||||||
const [keyword, setKeyword] = useState<string | null>(null);
|
const [keyword, setKeyword] = useAtom(SearchKeywordAtom);
|
||||||
const patterns = useAtomValue(PatternsAtom(keyword));
|
const patterns = useAtomValue(PatternsAtom);
|
||||||
|
const setPatternSelection = useSetAtom(SelectedPatternIdAtom);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createNewAction = useCallback(() => {
|
||||||
|
setPatternSelection(null);
|
||||||
|
navigate('/pattern-editor/new');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useDebounce(
|
useDebounce(
|
||||||
() => {
|
() => {
|
||||||
|
@ -36,7 +79,7 @@ const Patterns: FC = () => {
|
||||||
onChange={(evt) => setRawKeyword(evt.currentTarget.value)}
|
onChange={(evt) => setRawKeyword(evt.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="tonal secondary">
|
<button className="tonal secondary" onClick={createNewAction}>
|
||||||
<Icon icon="material-symbols-light:add" />
|
<Icon icon="material-symbols-light:add" />
|
||||||
New Pattern
|
New Pattern
|
||||||
</button>
|
</button>
|
||||||
|
@ -44,6 +87,9 @@ const Patterns: FC = () => {
|
||||||
<ScrollArea enableY>
|
<ScrollArea enableY>
|
||||||
<div className={styles.pattern_list}>
|
<div className={styles.pattern_list}>
|
||||||
{patterns.length === 0 && <div className="empty_prompt">No pattern found.</div>}
|
{patterns.length === 0 && <div className="empty_prompt">No pattern found.</div>}
|
||||||
|
{patterns.map((p) => (
|
||||||
|
<PatternCard key={p.id} pattern={p} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|
15
src/pages/CreatePattern.module.css
Normal file
15
src/pages/CreatePattern.module.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@layer pages {
|
||||||
|
.create_form {
|
||||||
|
border-radius: calc(var(--border-radius) * 2);
|
||||||
|
background-color: var(--color-surface-container);
|
||||||
|
}
|
||||||
|
.form_row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--spacing) * 3);
|
||||||
|
.pattern_name_input {
|
||||||
|
min-width: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/pages/CreatePattern.tsx
Normal file
79
src/pages/CreatePattern.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import cx from 'clsx';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { FC, useActionState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { NotificationType, ToastDuration, useNotification } from '../components/Notifications';
|
||||||
|
import { Pattern, SelectedPatternIdAtom, useSavePattern } from '../context/Patterns';
|
||||||
|
import styles from './CreatePattern.module.css';
|
||||||
|
|
||||||
|
const CreatePattern: FC = () => {
|
||||||
|
const { showToast } = useNotification();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const loadPattern = useSetAtom(SelectedPatternIdAtom);
|
||||||
|
const savePattern = useSavePattern();
|
||||||
|
|
||||||
|
const [errState, handleFormSubmit] = useActionState(async (state, formData) => {
|
||||||
|
const patternName = formData.get('pattern_name') as string | null;
|
||||||
|
|
||||||
|
if (patternName === null || patternName.length === 0) {
|
||||||
|
showToast(
|
||||||
|
NotificationType.ERROR,
|
||||||
|
'Please enter a pattern name.',
|
||||||
|
'material-symbols-light:error-outline',
|
||||||
|
ToastDuration.MEDIUM,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPattern = new Pattern();
|
||||||
|
newPattern.name = patternName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await savePattern(newPattern);
|
||||||
|
if (!updated) {
|
||||||
|
showToast(
|
||||||
|
NotificationType.ERROR,
|
||||||
|
'Failed to reload the created pattern. Please try again.',
|
||||||
|
'material-symbols-light:error-outline',
|
||||||
|
ToastDuration.MEDIUM,
|
||||||
|
);
|
||||||
|
loadPattern(null);
|
||||||
|
navigate('/library');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
loadPattern(newPattern.id);
|
||||||
|
navigate('/pattern-editor/edit');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[save pattern]', e);
|
||||||
|
loadPattern(null);
|
||||||
|
showToast(
|
||||||
|
NotificationType.ERROR,
|
||||||
|
'Failed to create pattern. Please try again.',
|
||||||
|
'material-symbols-light:error-outline',
|
||||||
|
ToastDuration.MEDIUM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('workspace', 'vertical', styles.create_form)}>
|
||||||
|
<div className="center">
|
||||||
|
<form action={handleFormSubmit} className={styles.form_row}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="pattern_name"
|
||||||
|
placeholder="pattern name"
|
||||||
|
className={cx(styles.pattern_name_input, errState && 'error')}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="filled">
|
||||||
|
Create New Pattern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreatePattern;
|
12
src/pages/PatternNavigator.tsx
Normal file
12
src/pages/PatternNavigator.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
import { SelectedPatternIdAtom } from '../context/Patterns';
|
||||||
|
|
||||||
|
const PatternNavigator: FC = () => {
|
||||||
|
const selected = useAtomValue(SelectedPatternIdAtom);
|
||||||
|
|
||||||
|
return selected === null ? <Navigate to="new" /> : <Navigate to="edit" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PatternNavigator;
|
Loading…
Reference in New Issue
Block a user