mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
Compare commits
2 Commits
fcc0267a46
...
palette-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a0cb589b | ||
|
|
9dbc861cdf |
3
.Jules/palette.md
Normal file
3
.Jules/palette.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 2025-05-14 - Password Visibility Toggle Accessibility
|
||||||
|
**Learning:** Icon-only buttons for toggling password visibility are often missing `aria-label` and `aria-pressed` states, making them unusable for screen reader users who need to verify their input.
|
||||||
|
**Action:** Always include dynamic `aria-label` (Switching between "Show password" and "Hide password") and `aria-pressed` state for password toggles.
|
||||||
108
provider/coinank/coinank_api/depth_ws.go
Normal file
108
provider/coinank/coinank_api/depth_ws.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package coinank_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"nofx/provider/coinank/coinank_enum"
|
||||||
|
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MainDepthWsUrl = "wss://ws.coinank.com/wsDepth/wsKline"
|
||||||
|
|
||||||
|
type DepthWs struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
DepthV3Ch <-chan *WsResult[DepthV3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepthWsConn connect ws , read data from DepthV3Ch
|
||||||
|
func DepthWsConn(ctx context.Context) (*DepthWs, error) {
|
||||||
|
conn, ch, err := depth_ws(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ws := &DepthWs{
|
||||||
|
conn: conn,
|
||||||
|
DepthV3Ch: ch,
|
||||||
|
}
|
||||||
|
return ws, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribe depth
|
||||||
|
func (ws *DepthWs) Subscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
|
||||||
|
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
|
||||||
|
info := SubscribeInfo{
|
||||||
|
Op: "subscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
json, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = websocket.Message.Send(ws.conn, json)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnSubscribe unsubscribe depth
|
||||||
|
func (ws *DepthWs) UnSubscribe(symbol string, exchange coinank_enum.Exchange, step string) error {
|
||||||
|
var args = "depthV3@" + symbol + "@" + string(exchange) + "@SWAP@" + step
|
||||||
|
info := SubscribeInfo{
|
||||||
|
Op: "unsubscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
json, err := json.Marshal(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = websocket.Message.Send(ws.conn, json)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close websocket
|
||||||
|
func (ws *DepthWs) Close() error {
|
||||||
|
return ws.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func depth_ws(ctx context.Context) (*websocket.Conn, <-chan *WsResult[DepthV3], error) {
|
||||||
|
config, err := websocket.NewConfig(MainDepthWsUrl, "http://localhost")
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
conn, err := config.DialContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
ch := make(chan *WsResult[DepthV3], 1024)
|
||||||
|
go depth_read(conn, ch)
|
||||||
|
return conn, ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func depth_read(conn *websocket.Conn, ch chan *WsResult[DepthV3]) {
|
||||||
|
defer conn.Close()
|
||||||
|
defer close(ch)
|
||||||
|
var msg string
|
||||||
|
for {
|
||||||
|
err := websocket.Message.Receive(conn, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var depth WsResult[DepthV3]
|
||||||
|
err = json.Unmarshal([]byte(msg), &depth)
|
||||||
|
if err == nil {
|
||||||
|
ch <- &depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DepthV3 struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Ts uint64 `json:"ts"`
|
||||||
|
Asks [][]string `json:"asks"`
|
||||||
|
Bids [][]string `json:"bids"`
|
||||||
|
}
|
||||||
42
provider/coinank/coinank_api/depth_ws_test.go
Normal file
42
provider/coinank/coinank_api/depth_ws_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package coinank_api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"nofx/provider/coinank/coinank_enum"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDepthWs(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
ws, err := DepthWsConn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for tickers := range ws.DepthV3Ch {
|
||||||
|
msg, err := json.Marshal(tickers)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("json err:", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(msg))
|
||||||
|
}
|
||||||
|
fmt.Println("DepthV3Ch closed")
|
||||||
|
}()
|
||||||
|
err = ws.Subscribe("BTCUSDT", coinank_enum.Binance, "0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("sub success")
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
err = ws.UnSubscribe("BTCUSDT", coinank_enum.Binance, "0.1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("unsub success")
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
ws.Close()
|
||||||
|
fmt.Println("cancel success")
|
||||||
|
}
|
||||||
@@ -334,6 +334,12 @@ export function LoginPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||||
|
aria-label={
|
||||||
|
showPassword
|
||||||
|
? t('hidePassword', language)
|
||||||
|
: t('showPassword', language)
|
||||||
|
}
|
||||||
|
aria-pressed={showPassword}
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -248,6 +248,12 @@ export function RegisterPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||||
|
aria-label={
|
||||||
|
showPassword
|
||||||
|
? t('hidePassword', language)
|
||||||
|
: t('showPassword', language)
|
||||||
|
}
|
||||||
|
aria-pressed={showPassword}
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
@@ -269,6 +275,12 @@ export function RegisterPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors"
|
||||||
|
aria-label={
|
||||||
|
showConfirmPassword
|
||||||
|
? t('hidePassword', language)
|
||||||
|
: t('showPassword', language)
|
||||||
|
}
|
||||||
|
aria-pressed={showConfirmPassword}
|
||||||
>
|
>
|
||||||
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -150,6 +150,12 @@ export function ResetPasswordPage() {
|
|||||||
onClick={() => setShowPassword(!showPassword)}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
|
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
aria-label={
|
||||||
|
showPassword
|
||||||
|
? t('hidePassword', language)
|
||||||
|
: t('showPassword', language)
|
||||||
|
}
|
||||||
|
aria-pressed={showPassword}
|
||||||
>
|
>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<EyeOff className="w-5 h-5" />
|
<EyeOff className="w-5 h-5" />
|
||||||
@@ -184,6 +190,12 @@ export function ResetPasswordPage() {
|
|||||||
}
|
}
|
||||||
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
|
className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon"
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
aria-label={
|
||||||
|
showConfirmPassword
|
||||||
|
? t('hidePassword', language)
|
||||||
|
: t('showPassword', language)
|
||||||
|
}
|
||||||
|
aria-pressed={showConfirmPassword}
|
||||||
>
|
>
|
||||||
{showConfirmPassword ? (
|
{showConfirmPassword ? (
|
||||||
<EyeOff className="w-5 h-5" />
|
<EyeOff className="w-5 h-5" />
|
||||||
|
|||||||
@@ -667,6 +667,8 @@ export const translations = {
|
|||||||
passwordRequired: 'Password is required',
|
passwordRequired: 'Password is required',
|
||||||
invalidEmail: 'Invalid email format',
|
invalidEmail: 'Invalid email format',
|
||||||
passwordTooShort: 'Password must be at least 6 characters',
|
passwordTooShort: 'Password must be at least 6 characters',
|
||||||
|
showPassword: 'Show password',
|
||||||
|
hidePassword: 'Hide password',
|
||||||
|
|
||||||
// Landing Page
|
// Landing Page
|
||||||
features: 'Features',
|
features: 'Features',
|
||||||
@@ -1834,6 +1836,8 @@ export const translations = {
|
|||||||
passwordRequired: '请输入密码',
|
passwordRequired: '请输入密码',
|
||||||
invalidEmail: '邮箱格式不正确',
|
invalidEmail: '邮箱格式不正确',
|
||||||
passwordTooShort: '密码至少需要6个字符',
|
passwordTooShort: '密码至少需要6个字符',
|
||||||
|
showPassword: '显示密码',
|
||||||
|
hidePassword: '隐藏密码',
|
||||||
|
|
||||||
// Landing Page
|
// Landing Page
|
||||||
features: '功能',
|
features: '功能',
|
||||||
|
|||||||
Reference in New Issue
Block a user