Skip to content

Latest commit

 

History

History
189 lines (135 loc) · 5.99 KB

File metadata and controls

189 lines (135 loc) · 5.99 KB

Code Style

Formatting (Prettier defaults)

  • Indentation: 2 spaces
  • Quotes: Double quotes (") for all strings
  • Semicolons: Always
  • Trailing commas: Always in multi-line constructs
  • Line width: Prettier default (80 chars)

File Naming

  • kebab-case for all files: vps-card.tsx, vm-payment-flow.tsx, account-settings.tsx
  • Components and pages: .tsx
  • Hooks and utilities without JSX: .ts

Imports

  • No enforced grouping or blank-line separation between groups
  • General tendency: CSS imports first, then external libraries, then local modules
  • No alphabetical ordering enforced

Components

Use function declarations, not arrow functions:

// Correct
export default function VpsCard({ spec }: { spec: VmTemplate }) {

// Avoid
const VpsCard = ({ spec }: { spec: VmTemplate }) => {
  • Pages and single-export components use export default function
  • Secondary exports from a file use named export

Types and Interfaces

  • PascalCase, no prefix (no I for interfaces, no T for types)
  • interface for object shapes, type for unions/intersections/simpler prop types
  • Both are acceptable; no strict rule between them
interface ModalProps { ... }
type AsyncButtonProps = { ... }
type PaymentFlowType = "lightning" | "revolut";

Enums

PascalCase name, PascalCase members, string values in lowercase:

enum DiskType {
  SSD = "ssd",
  HDD = "hdd",
}

State Management

  • Global state: Custom ExternalStore (from @snort/shared) + useSyncExternalStore. Singleton LoginState in src/login.ts.
  • Local state: Plain useState hooks
  • No Redux, Zustand, or other state libraries
  • Nostr context: SnortContext.Provider from @snort/system-react in main.tsx

API Calls

  • Use the LNVpsApi class from src/api.ts
  • Authenticated calls via login hook: login?.api.someMethod()
  • Unauthenticated calls: new LNVpsApi(ApiUrl, undefined)
  • Data fetching in useEffect or via the useCached hook
  • No React Query, SWR, or other data-fetching libraries

Custom Hooks

  • use prefix, function declarations, placed in src/hooks/
  • Return objects with named fields (not tuples/arrays):
export function useCached<T>(...): { data: T | undefined; loading: boolean; error: string | undefined; reloadNow: () => void }

Error Handling

try/catch with instanceof Error check, store message in useState<string>:

try {
  await login?.api.someAction();
} catch (e) {
  if (e instanceof Error) {
    setError(e.message);
  }
}
  • Display errors inline: {error && <b className="text-red-500">{error}</b>}
  • Non-critical errors: console.error(e) in catch block
  • The API class throws new Error(message) on non-OK responses

Null/Undefined Handling

  • Use optional chaining (?.) and nullish coalescing (??) extensively
  • Early returns for missing data:
if (!state) return <h2>No VM selected</h2>;
if (!login?.api) return;
  • Non-null assertion (!) used sparingly, only when guaranteed

Styling

  • Tailwind CSS utility classes inline in JSX
  • classnames library (imported as classNames) for conditional classes
  • No CSS modules, no styled-components
  • Theme colors: Always use cyber-* color classes for light/dark theme support. See src/index.css for available colors.
  • Only custom CSS file: src/components/spinner.css

Routing

  • react-router-dom v7 with createBrowserRouter in main.tsx
  • State-based navigation (passing objects via navigate("/path", { state }))
  • Access state via useLocation() with type assertion:
const location = useLocation() as { state?: VmInstance };

Comments

  • Minimal commenting; no JSDoc
  • Brief // comments only when context is needed
  • No block comment documentation

Private Class Members

Use ES private fields (#field, #method()) not TypeScript private keyword:

class LNVpsApi {
  #url: string;
  async #handleResponse<T>(rsp: Response): Promise<T> { ... }
}

News Article Language Tagging

Long-form articles (kind:30023) are tagged with language using NIP-32 self-labels (ISO-639-1):

["L", "ISO-639-1"],
["l", "zh", "ISO-639-1"]

d tag convention — addressable events require a unique kind:author:d key, so translations must use distinct d tags. Use a language suffix on the slug:

Version d tag language tag
English original my-article none (or ["l", "en", "ISO-639-1"])
Chinese translation my-article-zh ["l", "zh", "ISO-639-1"]
French translation my-article-fr ["l", "fr", "ISO-639-1"]

The client groups articles by canonical slug (stripping the -<lang> suffix) using src/utils/news-locale.ts, then picks the best version for the active locale: locale → en → unlabelled → any.

  • useLatestNews, useNewsPost, and NewsPage all apply this filtering automatically.
  • English articles do not need a language tag or suffix — they are the default fallback.

Translations

  • Use <FormattedMessage> component directly in JSX for all user-facing strings

  • Never use formatMessage({...}) as JSX children or text output — use <FormattedMessage> instead:

    // Wrong — function call in JSX children
    <button>{formatMessage({ defaultMessage: "Save" })}</button>
    
    // Correct — component in JSX
    <button><FormattedMessage defaultMessage="Save" /></button>
  • formatMessage({...}) is only acceptable for attributes that cannot accept JSX nodes (e.g., placeholder, title, aria-label, string arguments to confirm())

  • Auto-generated message IDs (no id or defaultMessage in defineMessage)

  • Never use defineMessage/Messages constant pattern except for API error messages

  • All dynamic strings in JSX should be wrapped in <FormattedMessage>

  • Translation files: src/locales/{locale}.json (auto-generated via yarn locale:extract)