easy-starter 1.2.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,127 @@
1
+ import React from "react";
2
+
3
+ export type ImgProps = React.ImgHTMLAttributes<HTMLImageElement> & {
4
+ /** Target width in pixels */
5
+ w?: number | string;
6
+ /** Target height in pixels */
7
+ h?: number | string;
8
+ /** Max width in pixels */
9
+ mw?: number | string;
10
+ /** Max height in pixels */
11
+ mh?: number | string;
12
+ /** Max width in pixels (alias) */
13
+ maxWidth?: number | string;
14
+ /** Max height in pixels (alias) */
15
+ maxHeight?: number | string;
16
+ /** Output image format (default: "webp") */
17
+ format?: "webp" | "jpeg" | "png";
18
+ /** Output image format (alias) */
19
+ type?: "webp" | "jpeg" | "png";
20
+ /** Quality from 1 to 100 */
21
+ quality?: number;
22
+ /** Quality from 1 to 100 (alias) */
23
+ q?: number;
24
+ /** Smooth resize processing */
25
+ smooth?: boolean;
26
+ /** Smooth resize processing (alias) */
27
+ s?: boolean;
28
+ };
29
+
30
+ export function imgSrc(
31
+ src: string,
32
+ options: {
33
+ w?: number | string;
34
+ h?: number | string;
35
+ mw?: number | string;
36
+ mh?: number | string;
37
+ format?: "webp" | "jpeg" | "png";
38
+ quality?: number;
39
+ smooth?: boolean;
40
+ }
41
+ ): string | undefined {
42
+ if (!src) return src;
43
+
44
+ const targetFormat = options.format || "webp";
45
+
46
+ try {
47
+ const [base, queryString] = src.split("?");
48
+ const params = new URLSearchParams(queryString || "");
49
+
50
+ if (options.w !== undefined && !params.has("w") && !params.has("width")) {
51
+ params.set("w", String(options.w));
52
+ }
53
+ if (options.h !== undefined && !params.has("h") && !params.has("height")) {
54
+ params.set("h", String(options.h));
55
+ }
56
+ if (options.mw !== undefined && !params.has("mw") && !params.has("maxWidth")) {
57
+ params.set("mw", String(options.mw));
58
+ }
59
+ if (options.mh !== undefined && !params.has("mh") && !params.has("maxHeight")) {
60
+ params.set("mh", String(options.mh));
61
+ }
62
+ if (options.quality !== undefined && !params.has("q") && !params.has("quality")) {
63
+ params.set("q", String(options.quality));
64
+ }
65
+ if (options.smooth !== undefined && !params.has("s") && !params.has("smooth")) {
66
+ params.set("s", String(options.smooth));
67
+ }
68
+ if (targetFormat && !params.has("format") && !params.has("type") && !params.has("ext")) {
69
+ params.set("format", targetFormat);
70
+ }
71
+
72
+ const finalQuery = params.toString();
73
+ return finalQuery ? `${base}?${finalQuery}` : base;
74
+ } catch {
75
+ return src;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Smart image component wrapping HTML <img> with automatic easy-image-resizer URL formatting.
81
+ * Defaults to WebP format with optional width/height/quality scaling options.
82
+ */
83
+ export function Img({
84
+ src,
85
+ width,
86
+ height,
87
+ w,
88
+ h,
89
+ mw,
90
+ mh,
91
+ maxWidth,
92
+ maxHeight,
93
+ format,
94
+ type,
95
+ quality,
96
+ q,
97
+ smooth,
98
+ s,
99
+ ...restProps
100
+ }: ImgProps) {
101
+ const finalW = w ?? width;
102
+ const finalH = h ?? height;
103
+ const finalMw = mw ?? maxWidth;
104
+ const finalMh = mh ?? maxHeight;
105
+ const finalFormat = format ?? type ?? "webp";
106
+ const finalQuality = q ?? quality;
107
+ const finalSmooth = s ?? smooth;
108
+
109
+ const finalSrc = imgSrc(src || "", {
110
+ w: finalW,
111
+ h: finalH,
112
+ mw: finalMw,
113
+ mh: finalMh,
114
+ format: finalFormat,
115
+ quality: finalQuality,
116
+ smooth: finalSmooth,
117
+ });
118
+
119
+ return <img
120
+ src={finalSrc}
121
+ width={finalW}
122
+ height={finalH}
123
+ {...restProps}
124
+ />;
125
+ };
126
+
127
+ export default Img;
@@ -1,12 +1,35 @@
1
1
  import React from "react";
2
- import { createRoot, hydrateRoot } from "react-dom/client";
2
+ import { createRoot } from "react-dom/client";
3
+ // --- SSR START ---
4
+ import { hydrateRoot } from "react-dom/client";
5
+ // --- SSR END ---
3
6
 
4
7
  import { handleError } from "./services/api";
5
8
 
6
9
  import App from "./App";
10
+ import "./styles.css";
11
+
12
+ // --- VIEWPORT START ---
13
+ // Fix iOS safari issues with viewport
14
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
15
+ const updateViewport = () => {
16
+ const metaViewport = document.querySelector('meta[name="viewport"]');
17
+ if (metaViewport) {
18
+ if (window.screen.width <= 600) {
19
+ metaViewport.setAttribute('content', 'width=600, user-scalable=no');
20
+ } else {
21
+ metaViewport.setAttribute('content', 'width=device-width');
22
+ }
23
+ }
24
+ }
25
+ updateViewport();
26
+ window.addEventListener('resize', updateViewport);
27
+ }
28
+ // --- VIEWPORT END ---
7
29
 
8
30
  const rootElement = document.getElementById("root") as HTMLElement;
9
31
 
32
+ // --- SSR START ---
10
33
  if (process.env.NODE_ENV === "development") {
11
34
  // We use createRoot because we often don't want to deal with full SSR hydration quirks
12
35
  // during local frontend development (like Hot Module Replacement causing mismatches).
@@ -21,3 +44,7 @@ if (process.env.NODE_ENV === "development") {
21
44
  onRecoverableError: handleError,
22
45
  });
23
46
  }
47
+ // --- SSR END ---
48
+ // --- NO_SSR START ---
49
+ createRoot(rootElement).render(<App />);
50
+ // --- NO_SSR END ---
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import React, { useState } from "react";
2
2
  import { Link } from "easy-page-router/react";
3
3
 
4
4
  import { api, useSSRApi } from "../services/api";
@@ -6,8 +6,20 @@ import { useHead } from "../services/common";
6
6
 
7
7
  import { Item } from "../../types";
8
8
 
9
+ // --- AUTH START ---
10
+ import { useUser, UserDialog } from "easy-user-auth/react";
11
+ import { UserProfile } from "../../types";
12
+ // --- AUTH END ---
13
+
9
14
  export default function Index() {
10
- useHead({ title: "Welcome to easy-starter", description: "The ultimate foundation for your next project." });
15
+ useHead({
16
+ title: "Welcome to easy-starter",
17
+ description: "The ultimate foundation for your next project."
18
+ });
19
+
20
+ // --- AUTH START ---
21
+ const { isLoggedIn, user, loginUser, showUserDialog } = useUser<UserProfile>();
22
+ // --- AUTH END ---
11
23
 
12
24
  // This will run on the server, fetch the data, embed it in the HTML,
13
25
  // and hydrate perfectly on the client. Fully type-safe!
@@ -47,17 +59,35 @@ export default function Index() {
47
59
 
48
60
  return (
49
61
  <main>
62
+ {/* --- AUTH START --- */}
63
+ <div className="auth-bar">
64
+ {isLoggedIn ? (
65
+ <div className="auth-user-info">
66
+ <span>Logged in as: <strong>{user.name} ({user.role})</strong></span>
67
+ <button onClick={() => loginUser(null)}>Log Out</button>
68
+ </div>
69
+ ) : (
70
+ <button onClick={showUserDialog}>Log In / Sign Up</button>
71
+ )}
72
+ </div>
73
+ {/* --- AUTH END --- */}
74
+
50
75
  <header>
51
76
  <figure></figure>
52
77
  <h1>Welcome to easy-starter!</h1>
53
78
  <p>The ultimate foundation for your next project.</p>
54
- <Link href="/admin">
55
- Go to Admin Dashboard
56
- </Link>
79
+
80
+ {/* --- ADMIN START --- */}
81
+ <div className="admin-link-wrapper">
82
+ <Link href="/admin">
83
+ → Go to Admin Dashboard
84
+ </Link>
85
+ </div>
86
+ {/* --- ADMIN END --- */}
57
87
  </header>
58
88
 
59
89
  <section>
60
- <h2>Items (SSR Hook)</h2>
90
+ <h2>Items</h2>
61
91
  {loading && <p>Loading from server...</p>}
62
92
  {error && <p>Error loading items: {error.message}</p>}
63
93
  {items && (
@@ -6,7 +6,7 @@ export default function NotFound() {
6
6
  useHead({ title: "404 Not Found", description: "Page you are looking for does not exist." });
7
7
 
8
8
  return (
9
- <div style={{ padding: "40px", textAlign: "center", fontFamily: "sans-serif" }}>
9
+ <div className="not-found">
10
10
  <h1>404</h1>
11
11
  <p>Oops, page not found.</p>
12
12
  <Link href="/">Go back home</Link>
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { useRouter } from "easy-page-router/react";
3
+ import { useUser } from "easy-user-auth/react";
4
+ import { useHead } from "../services/common";
5
+ import { UserProfile } from "../../types";
6
+
7
+ export default function ResetPasswordPage() {
8
+ useHead({ title: "Reset Password", description: "Enter a new password for your account." });
9
+
10
+ const { api, loginUser, dict } = useUser<UserProfile>();
11
+ const { push } = useRouter();
12
+
13
+ const [token, setToken] = useState<string | null>(null);
14
+ const [password, setPassword] = useState("");
15
+ const [passwordAgain, setPasswordAgain] = useState("");
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [success, setSuccess] = useState<boolean>(false);
18
+ const [loading, setLoading] = useState<boolean>(false);
19
+
20
+ useEffect(() => {
21
+ if (typeof window !== "undefined") {
22
+ const params = new URLSearchParams(window.location.search);
23
+ const tok = params.get("token");
24
+ if (tok) {
25
+ setToken(tok);
26
+ } else {
27
+ setError("Reset token is missing in the URL.");
28
+ }
29
+ }
30
+ }, []);
31
+
32
+ let validationError = "";
33
+ if (password.length > 0 && password.length < 6) {
34
+ validationError = dict.userWrongPasswordLength;
35
+ } else if (password && passwordAgain && password !== passwordAgain) {
36
+ validationError = dict.userWrongPasswordMatch;
37
+ }
38
+
39
+ const handleSubmit = async (e: React.FormEvent) => {
40
+ e.preventDefault();
41
+ if (validationError || !password || !passwordAgain || !token) return;
42
+
43
+ setLoading(true);
44
+ setError(null);
45
+
46
+ const [userResult, err] = await api.updateForgottenPassword({ token, password });
47
+
48
+ if (err) {
49
+ setError(dict.userForgottenPasswordErrorChange);
50
+ } else if (userResult) {
51
+ setSuccess(true);
52
+ setTimeout(() => {
53
+ loginUser(userResult);
54
+ push("/");
55
+ }, 2000);
56
+ }
57
+ setLoading(false);
58
+ };
59
+
60
+ return (
61
+ <main className="reset-password-main">
62
+ <h2 className="easy-user-auth-title">🔑 {dict.userForgottenPasswordChange}</h2>
63
+
64
+ {error && !token && (
65
+ <div className="easy-user-auth-alert">{error}</div>
66
+ )}
67
+
68
+ {success ? (
69
+ <div className="easy-user-auth-alert alert-success">
70
+ {dict.userForgottenPasswordSuccessChange}
71
+ </div>
72
+ ) : (
73
+ token && (
74
+ <form onSubmit={handleSubmit} className="easy-user-auth-form">
75
+ <div className="easy-user-auth-form-input">
76
+ <label className="easy-user-auth-label">{dict.password}</label>
77
+ <input
78
+ type="password"
79
+ className="easy-user-auth-input"
80
+ value={password}
81
+ onChange={(e) => setPassword(e.target.value)}
82
+ required
83
+ maxLength={50}
84
+ />
85
+ </div>
86
+
87
+ <div className="easy-user-auth-form-input">
88
+ <label className="easy-user-auth-label">{dict.passwordAgain}</label>
89
+ <input
90
+ type="password"
91
+ className="easy-user-auth-input"
92
+ value={passwordAgain}
93
+ onChange={(e) => setPasswordAgain(e.target.value)}
94
+ required
95
+ maxLength={50}
96
+ />
97
+ </div>
98
+
99
+ {error && <div className="easy-user-auth-alert">{error}</div>}
100
+ {validationError && <p className="easy-user-auth-info text-error">{validationError}</p>}
101
+
102
+ <div className="btn-group">
103
+ <button
104
+ type="submit"
105
+ className="easy-user-auth-submit-btn"
106
+ disabled={!!validationError || !password || !passwordAgain || loading}
107
+ >
108
+ {loading ? "Updating..." : dict.change}
109
+ </button>
110
+ <button
111
+ type="button"
112
+ className="easy-user-auth-back-btn"
113
+ onClick={() => push("/")}
114
+ disabled={loading}
115
+ >
116
+ Cancel
117
+ </button>
118
+ </div>
119
+ </form>
120
+ )
121
+ )}
122
+ </main>
123
+ );
124
+ }