36 changed files with 2215 additions and 4529 deletions
@ -0,0 +1,74 @@ |
|||
import React from 'react'; |
|||
|
|||
import { AnimatePresence, motion } from 'framer-motion'; |
|||
|
|||
import { Disclosure } from '@headlessui/react'; |
|||
import { ChevronDownIcon } from '@heroicons/react/outline'; |
|||
|
|||
interface DropdownProps { |
|||
icon: JSX.Element; |
|||
title: string; |
|||
content: JSX.Element; |
|||
fallbackMessage: string; |
|||
} |
|||
|
|||
export const Dropdown = (props: DropdownProps): JSX.Element => { |
|||
return ( |
|||
<Disclosure> |
|||
{({ open }) => ( |
|||
<> |
|||
<Disclosure.Button className="bg-white flex w-full text-lg font-medium justify-between p-3 border-b hover:bg-gray-200 cursor-pointer"> |
|||
<div className="flex"> |
|||
<motion.div |
|||
className="my-auto mr-2" |
|||
variants={{ |
|||
initial: { rotate: -90 }, |
|||
animate: { |
|||
rotate: 0, |
|||
}, |
|||
}} |
|||
initial="initial" |
|||
animate={open ? 'animate' : 'initial'} |
|||
> |
|||
<ChevronDownIcon className="w-5 h-5" /> |
|||
</motion.div> |
|||
{props.icon} |
|||
{props.title} |
|||
</div> |
|||
</Disclosure.Button> |
|||
|
|||
<AnimatePresence> |
|||
{open && ( |
|||
<Disclosure.Panel |
|||
as={motion.div} |
|||
static |
|||
initial={{ |
|||
height: 0, |
|||
}} |
|||
animate={{ |
|||
height: 'auto', |
|||
}} |
|||
exit={{ |
|||
height: 0, |
|||
}} |
|||
className="shadow-inner" |
|||
> |
|||
<React.Suspense |
|||
fallback={ |
|||
<div className="flex border-b border-gray-300"> |
|||
<div className="m-auto p-3 text-gray-500"> |
|||
{props.fallbackMessage} |
|||
</div> |
|||
</div> |
|||
} |
|||
> |
|||
{props.content} |
|||
</React.Suspense> |
|||
</Disclosure.Panel> |
|||
)} |
|||
</AnimatePresence> |
|||
</> |
|||
)} |
|||
</Disclosure> |
|||
); |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
import type { TypedUseSelectorHook } from 'react-redux'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
|
|||
import type { AppDispatch, RootState } from '../store'; |
|||
|
|||
export const useAppDispatch = () => useDispatch<AppDispatch>(); |
|||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; |
|||
@ -1,42 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import Translations_EN from '../translations/en'; |
|||
import Translations_JP from '../translations/jp'; |
|||
import Translations_PT from '../translations/pt'; |
|||
import type { |
|||
languageTemplate, |
|||
TranslationsContextData, |
|||
} from '../translations/TranslationsContext'; |
|||
import { LanguageEnum } from '../translations/TranslationsContext'; |
|||
|
|||
export const useTranslationsContextValue = (): TranslationsContextData => { |
|||
const [currentLanguage, setcurrentLanguage] = React.useState<LanguageEnum>( |
|||
LanguageEnum.ENGLISH, |
|||
); |
|||
const [translation, setTranslation] = |
|||
React.useState<languageTemplate>(Translations_EN); |
|||
|
|||
const setLanguage = React.useCallback( |
|||
(language: LanguageEnum) => { |
|||
setcurrentLanguage(language); |
|||
switch (language) { |
|||
case LanguageEnum.ENGLISH: |
|||
setTranslation(Translations_EN); |
|||
break; |
|||
case LanguageEnum.JAPANESE: |
|||
setTranslation(Translations_JP); |
|||
break; |
|||
case LanguageEnum.PORTUGUESE: |
|||
setTranslation(Translations_PT); |
|||
break; |
|||
} |
|||
}, |
|||
[setcurrentLanguage, setTranslation], |
|||
); |
|||
|
|||
return { |
|||
language: currentLanguage, |
|||
setLanguage: setLanguage, |
|||
translations: translation, |
|||
}; |
|||
}; |
|||
@ -0,0 +1,29 @@ |
|||
import { createSlice } from '@reduxjs/toolkit'; |
|||
|
|||
interface AppState { |
|||
sidebarOpen: boolean; |
|||
} |
|||
|
|||
const initialState: AppState = { |
|||
sidebarOpen: true, |
|||
}; |
|||
|
|||
export const appSlice = createSlice({ |
|||
name: 'auth', |
|||
initialState, |
|||
reducers: { |
|||
openSidebar(state) { |
|||
state.sidebarOpen = true; |
|||
}, |
|||
closeSidebar(state) { |
|||
state.sidebarOpen = false; |
|||
}, |
|||
toggleSidebar(state) { |
|||
state.sidebarOpen = !state.sidebarOpen; |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
export const { openSidebar, closeSidebar, toggleSidebar } = appSlice.actions; |
|||
|
|||
export default appSlice.reducer; |
|||
@ -0,0 +1,24 @@ |
|||
import type { PayloadAction } from '@reduxjs/toolkit'; |
|||
import { createSlice } from '@reduxjs/toolkit'; |
|||
|
|||
interface AppState { |
|||
myId: number; |
|||
} |
|||
|
|||
const initialState: AppState = { |
|||
myId: 0, |
|||
}; |
|||
|
|||
export const meshtasticSlice = createSlice({ |
|||
name: 'meshtastic', |
|||
initialState, |
|||
reducers: { |
|||
setMyId: (state, action: PayloadAction<number>) => { |
|||
state.myId = action.payload; |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
export const { setMyId } = meshtasticSlice.actions; |
|||
|
|||
export default meshtasticSlice.reducer; |
|||
@ -1,24 +1,14 @@ |
|||
import type { Protobuf } from '@meshtastic/meshtasticjs'; |
|||
import { configureStore, createSlice } from '@reduxjs/toolkit'; |
|||
import { configureStore } from '@reduxjs/toolkit'; |
|||
|
|||
const nodesSlice = createSlice({ |
|||
name: 'nodes', |
|||
initialState: { |
|||
members: [], |
|||
}, |
|||
reducers: { |
|||
addMember: (state: Protobuf.NodeInfo[], node: Protobuf.NodeInfo) => { |
|||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
|||
// doesn't actually mutate the state because it uses the Immer library,
|
|||
// which detects changes to a "draft state" and produces a brand new
|
|||
// immutable state based off those changes
|
|||
state.push(node); |
|||
}, |
|||
}, |
|||
}); |
|||
import appSlice from './slices/appSlice'; |
|||
import meshtasticSlice from './slices/meshtasticSlice'; |
|||
|
|||
export default configureStore({ |
|||
export const store = configureStore({ |
|||
reducer: { |
|||
nodes: nodesSlice.reducer, |
|||
app: appSlice, |
|||
meshtastic: meshtasticSlice, |
|||
}, |
|||
}); |
|||
|
|||
export type RootState = ReturnType<typeof store.getState>; |
|||
export type AppDispatch = typeof store.dispatch; |
|||
|
|||
@ -0,0 +1,19 @@ |
|||
import i18n from 'i18next'; |
|||
import detector from 'i18next-browser-languagedetector'; |
|||
import { initReactI18next } from 'react-i18next'; |
|||
|
|||
import en from './translations/en.json'; |
|||
|
|||
i18n |
|||
.use(detector) |
|||
.use(initReactI18next) |
|||
.init({ |
|||
fallbackLng: 'en', |
|||
resources: { |
|||
en: { |
|||
translation: en, |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
export default i18n; |
|||
@ -1,41 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import Translations_EN from './en'; |
|||
|
|||
export interface languageTemplate { |
|||
no_messages_message: string; |
|||
ui_settings_title: string; |
|||
nodes_title: string; |
|||
color_scheme_title: string; |
|||
language_title: string; |
|||
device_settings_title: string; |
|||
device_channels_title: string; |
|||
device_region_title: string; |
|||
device_wifi_ssid: string; |
|||
device_wifi_psk: string; |
|||
save_changes_button: string; |
|||
no_nodes_message: string; |
|||
no_message_placeholder: string; |
|||
} |
|||
|
|||
export enum LanguageEnum { |
|||
ENGLISH, |
|||
JAPANESE, |
|||
PORTUGUESE, |
|||
} |
|||
|
|||
export interface TranslationsContextData { |
|||
language: LanguageEnum; |
|||
setLanguage: (postId: number) => void; |
|||
translations: languageTemplate; |
|||
} |
|||
|
|||
export const translationsContextDefaultValue: TranslationsContextData = { |
|||
language: LanguageEnum.ENGLISH, |
|||
setLanguage: () => null, |
|||
translations: Translations_EN, |
|||
}; |
|||
|
|||
export const TranslationsContext = React.createContext<TranslationsContextData>( |
|||
translationsContextDefaultValue, |
|||
); |
|||
@ -0,0 +1,22 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "Enter Message", |
|||
"no_messages": "No messages yet", |
|||
"no_nodes": "No nodes found" |
|||
}, |
|||
"strings": { |
|||
"nodes": "Nodes", |
|||
"color_scheme": "Color scheme", |
|||
"language": "Language", |
|||
"device_region": "Device region", |
|||
"wifi_ssid": "WiFi SSID", |
|||
"wifi_psk": "WiFi PSK", |
|||
"save_changes": "Save changes" |
|||
}, |
|||
"settings": { |
|||
"ui": "UI Settings", |
|||
"device": "Device Settings", |
|||
"channel": "Channels" |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
import type { languageTemplate } from './TranslationsContext'; |
|||
|
|||
export default { |
|||
no_messages_message: 'No messages yet', |
|||
ui_settings_title: 'UI Settings', |
|||
nodes_title: 'Nodes', |
|||
device_settings_title: 'Device Settings', |
|||
device_channels_title: 'Channels', |
|||
color_scheme_title: 'Color scheme', |
|||
language_title: 'Language', |
|||
device_region_title: 'Device Region', |
|||
device_wifi_ssid: 'WiFi SSID', |
|||
device_wifi_psk: 'WiFi PSK', |
|||
save_changes_button: 'Save changes', |
|||
no_nodes_message: 'No nodes found', |
|||
no_message_placeholder: 'Enter Message', |
|||
} as languageTemplate; |
|||
@ -0,0 +1,22 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "メッセージを入力してください", |
|||
"no_messages": "まだメッセージはありません", |
|||
"no_nodes": "ノードが見つかりません" |
|||
}, |
|||
"strings": { |
|||
"nodes": "ノード", |
|||
"color_scheme": "カラースキーム", |
|||
"language": "言語", |
|||
"device_region": "デバイスリージョン", |
|||
"wifi_ssid": "WiFi名", |
|||
"wifi_psk": "WiFiパスワード", |
|||
"save_changes": "変更内容を保存" |
|||
}, |
|||
"settings": { |
|||
"ui": "UI設定", |
|||
"device": "デバイスの設定", |
|||
"channel": "#################" |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
import type { languageTemplate } from './TranslationsContext'; |
|||
|
|||
export default { |
|||
no_messages_message: 'まだメッセージはありません', |
|||
ui_settings_title: 'UI設定', |
|||
nodes_title: 'ノード', |
|||
device_settings_title: 'デバイスの設定', |
|||
color_scheme_title: 'カラースキーム', |
|||
language_title: '言語', |
|||
device_region_title: 'デバイスリージョン', |
|||
device_wifi_ssid: 'WiFi名', |
|||
device_wifi_psk: 'WiFiパスワード', |
|||
save_changes_button: '変更内容を保存', |
|||
no_nodes_message: 'ノードが見つかりません', |
|||
no_message_placeholder: 'メッセージを入力してください', |
|||
} as languageTemplate; |
|||
@ -0,0 +1,22 @@ |
|||
{ |
|||
"errors": {}, |
|||
"placeholder": { |
|||
"message": "Entre mensagem", |
|||
"no_messages": "Não a mensagens ainda", |
|||
"no_nodes": "Nenhum nó foi encontrado" |
|||
}, |
|||
"strings": { |
|||
"nodes": "Nós", |
|||
"color_scheme": "Esquema de cores", |
|||
"language": "Idioma", |
|||
"device_region": "Região do dispositivo", |
|||
"wifi_ssid": "Nome do WiFi", |
|||
"wifi_psk": "Senha do WiFi", |
|||
"save_changes": "Salvar alterações" |
|||
}, |
|||
"settings": { |
|||
"ui": "Configurações da Interface", |
|||
"device": "Configurações do dispositivo", |
|||
"channel": "Canais" |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
import type { languageTemplate } from './TranslationsContext'; |
|||
|
|||
export default { |
|||
no_messages_message: 'Não a mensagens ainda', |
|||
ui_settings_title: 'Configurações da Interface', |
|||
nodes_title: 'Nós', |
|||
device_settings_title: 'Configurações do dispositivo', |
|||
device_channels_title: 'Canais', |
|||
color_scheme_title: 'Esquema de cores', |
|||
language_title: 'Idioma', |
|||
device_region_title: 'Região do dispositivo', |
|||
device_wifi_ssid: 'Nome do WiFi', |
|||
device_wifi_psk: 'Senha do WiFi', |
|||
save_changes_button: 'Salvar alterações', |
|||
no_nodes_message: 'Nenhum nó foi encontrado', |
|||
no_message_placeholder: 'Entre mensagem', |
|||
} as languageTemplate; |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue