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 } from '@reduxjs/toolkit'; |
||||
import { configureStore, createSlice } from '@reduxjs/toolkit'; |
|
||||
|
|
||||
const nodesSlice = createSlice({ |
import appSlice from './slices/appSlice'; |
||||
name: 'nodes', |
import meshtasticSlice from './slices/meshtasticSlice'; |
||||
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); |
|
||||
}, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
export default configureStore({ |
export const store = configureStore({ |
||||
reducer: { |
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