feat: ship minecraft theme updates and platform workflow improvements
这个提交包含在:
@@ -13,6 +13,9 @@ class AdminController : public drogon::HttpController<AdminController> {
|
|||||||
ADD_METHOD_TO(AdminController::updateUserRating,
|
ADD_METHOD_TO(AdminController::updateUserRating,
|
||||||
"/api/v1/admin/users/{1}/rating",
|
"/api/v1/admin/users/{1}/rating",
|
||||||
drogon::Patch);
|
drogon::Patch);
|
||||||
|
ADD_METHOD_TO(AdminController::deleteUser,
|
||||||
|
"/api/v1/admin/users/{1}",
|
||||||
|
drogon::Delete);
|
||||||
ADD_METHOD_TO(AdminController::listRedeemItems, "/api/v1/admin/redeem-items", drogon::Get);
|
ADD_METHOD_TO(AdminController::listRedeemItems, "/api/v1/admin/redeem-items", drogon::Get);
|
||||||
ADD_METHOD_TO(AdminController::createRedeemItem, "/api/v1/admin/redeem-items", drogon::Post);
|
ADD_METHOD_TO(AdminController::createRedeemItem, "/api/v1/admin/redeem-items", drogon::Post);
|
||||||
ADD_METHOD_TO(AdminController::updateRedeemItem,
|
ADD_METHOD_TO(AdminController::updateRedeemItem,
|
||||||
@@ -32,6 +35,9 @@ class AdminController : public drogon::HttpController<AdminController> {
|
|||||||
void updateUserRating(const drogon::HttpRequestPtr& req,
|
void updateUserRating(const drogon::HttpRequestPtr& req,
|
||||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||||
int64_t user_id);
|
int64_t user_id);
|
||||||
|
void deleteUser(const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||||
|
int64_t user_id);
|
||||||
|
|
||||||
void listRedeemItems(const drogon::HttpRequestPtr& req,
|
void listRedeemItems(const drogon::HttpRequestPtr& req,
|
||||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class UserService {
|
|||||||
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100);
|
std::vector<domain::GlobalLeaderboardEntry> GlobalLeaderboard(int limit = 100);
|
||||||
UserListResult ListUsers(int page, int page_size);
|
UserListResult ListUsers(int page, int page_size);
|
||||||
void SetRating(int64_t user_id, int rating);
|
void SetRating(int64_t user_id, int rating);
|
||||||
|
void DeleteUser(int64_t user_id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
db::SqliteDb& db_;
|
db::SqliteDb& db_;
|
||||||
|
|||||||
@@ -192,6 +192,42 @@ void AdminController::updateUserRating(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AdminController::deleteUser(
|
||||||
|
const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
||||||
|
int64_t user_id) {
|
||||||
|
try {
|
||||||
|
const auto admin_user_id = RequireAdminUserId(req, cb);
|
||||||
|
if (!admin_user_id.has_value()) return;
|
||||||
|
|
||||||
|
if (*admin_user_id == user_id) {
|
||||||
|
cb(JsonError(drogon::k400BadRequest, "cannot delete current admin user"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
services::UserService users(csp::AppState::Instance().db());
|
||||||
|
const auto target = users.GetById(user_id);
|
||||||
|
if (!target.has_value()) {
|
||||||
|
cb(JsonError(drogon::k404NotFound, "user not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target->username == "admin") {
|
||||||
|
cb(JsonError(drogon::k400BadRequest, "cannot delete reserved admin user"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
users.DeleteUser(user_id);
|
||||||
|
|
||||||
|
Json::Value payload;
|
||||||
|
payload["id"] = Json::Int64(user_id);
|
||||||
|
payload["username"] = target->username;
|
||||||
|
payload["deleted"] = true;
|
||||||
|
cb(JsonOk(payload));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AdminController::listRedeemItems(
|
void AdminController::listRedeemItems(
|
||||||
const drogon::HttpRequestPtr& req,
|
const drogon::HttpRequestPtr& req,
|
||||||
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ Json::Value BuildOpenApiSpec() {
|
|||||||
paths["/api/v1/submissions/{id}/analysis"]["post"]["summary"] = "提交评测建议(LLM)";
|
paths["/api/v1/submissions/{id}/analysis"]["post"]["summary"] = "提交评测建议(LLM)";
|
||||||
paths["/api/v1/admin/users"]["get"]["summary"] = "管理员用户列表";
|
paths["/api/v1/admin/users"]["get"]["summary"] = "管理员用户列表";
|
||||||
paths["/api/v1/admin/users/{id}/rating"]["patch"]["summary"] = "管理员修改用户积分";
|
paths["/api/v1/admin/users/{id}/rating"]["patch"]["summary"] = "管理员修改用户积分";
|
||||||
|
paths["/api/v1/admin/users/{id}"]["delete"]["summary"] = "管理员删除用户";
|
||||||
paths["/api/v1/admin/redeem-items"]["get"]["summary"] = "管理员查看积分兑换物品";
|
paths["/api/v1/admin/redeem-items"]["get"]["summary"] = "管理员查看积分兑换物品";
|
||||||
paths["/api/v1/admin/redeem-items"]["post"]["summary"] = "管理员新增积分兑换物品";
|
paths["/api/v1/admin/redeem-items"]["post"]["summary"] = "管理员新增积分兑换物品";
|
||||||
paths["/api/v1/admin/redeem-items/{id}"]["patch"]["summary"] = "管理员修改积分兑换物品";
|
paths["/api/v1/admin/redeem-items/{id}"]["patch"]["summary"] = "管理员修改积分兑换物品";
|
||||||
|
|||||||
@@ -138,7 +138,13 @@ void ProblemController::getDraft(
|
|||||||
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
services::ProblemWorkspaceService svc(csp::AppState::Instance().db());
|
||||||
const auto draft = svc.GetDraft(*user_id, problem_id);
|
const auto draft = svc.GetDraft(*user_id, problem_id);
|
||||||
if (!draft.has_value()) {
|
if (!draft.has_value()) {
|
||||||
cb(JsonError(drogon::k404NotFound, "draft not found"));
|
Json::Value payload;
|
||||||
|
payload["language"] = "cpp";
|
||||||
|
payload["code"] = "";
|
||||||
|
payload["stdin"] = "";
|
||||||
|
payload["updated_at"] = Json::nullValue;
|
||||||
|
payload["exists"] = false;
|
||||||
|
cb(JsonOk(payload));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +153,7 @@ void ProblemController::getDraft(
|
|||||||
payload["code"] = draft->code;
|
payload["code"] = draft->code;
|
||||||
payload["stdin"] = draft->stdin_text;
|
payload["stdin"] = draft->stdin_text;
|
||||||
payload["updated_at"] = Json::Int64(draft->updated_at);
|
payload["updated_at"] = Json::Int64(draft->updated_at);
|
||||||
|
payload["exists"] = true;
|
||||||
cb(JsonOk(payload));
|
cb(JsonOk(payload));
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
||||||
|
|||||||
@@ -126,4 +126,18 @@ void UserService::SetRating(int64_t user_id, int rating) {
|
|||||||
if (sqlite3_changes(db) <= 0) throw std::runtime_error("user not found");
|
if (sqlite3_changes(db) <= 0) throw std::runtime_error("user not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UserService::DeleteUser(int64_t user_id) {
|
||||||
|
if (user_id <= 0) throw std::runtime_error("invalid user_id");
|
||||||
|
|
||||||
|
sqlite3* db = db_.raw();
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
const char* sql = "DELETE FROM users WHERE id=?";
|
||||||
|
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
|
||||||
|
"prepare delete user");
|
||||||
|
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
|
||||||
|
CheckSqlite(sqlite3_step(stmt), db, "exec delete user");
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
if (sqlite3_changes(db) <= 0) throw std::runtime_error("user not found");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace csp::services
|
} // namespace csp::services
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ export const runtime = "nodejs";
|
|||||||
|
|
||||||
const CACHE_DIR = process.env.CSP_IMAGE_CACHE_DIR ?? "/tmp/csp-image-cache";
|
const CACHE_DIR = process.env.CSP_IMAGE_CACHE_DIR ?? "/tmp/csp-image-cache";
|
||||||
const MAX_BYTES = 5 * 1024 * 1024;
|
const MAX_BYTES = 5 * 1024 * 1024;
|
||||||
|
const IMAGE_EXT_TO_TYPE: Record<string, string> = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
};
|
||||||
|
|
||||||
function toArrayBuffer(view: Uint8Array): ArrayBuffer {
|
function toArrayBuffer(view: Uint8Array): ArrayBuffer {
|
||||||
return view.buffer.slice(
|
return view.buffer.slice(
|
||||||
@@ -28,6 +38,25 @@ function pickExt(urlObj: URL, contentType: string): string {
|
|||||||
return ".img";
|
return ".img";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function inferImageType(urlObj: URL, contentType: string): string {
|
||||||
|
const raw = contentType.split(";")[0].trim().toLowerCase();
|
||||||
|
if (raw.startsWith("image/")) return raw;
|
||||||
|
const ext = path.extname(urlObj.pathname || "").toLowerCase();
|
||||||
|
return IMAGE_EXT_TO_TYPE[ext] ?? raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeImage(urlObj: URL, contentType: string): boolean {
|
||||||
|
if (contentType.startsWith("image/")) return true;
|
||||||
|
const ext = path.extname(urlObj.pathname || "").toLowerCase();
|
||||||
|
return Boolean(IMAGE_EXT_TO_TYPE[ext]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectToTarget(target: URL): NextResponse {
|
||||||
|
const res = NextResponse.redirect(target.toString(), 307);
|
||||||
|
res.headers.set("Cache-Control", "no-store");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
async function readCachedByKey(
|
async function readCachedByKey(
|
||||||
key: string
|
key: string
|
||||||
): Promise<{ data: Uint8Array; contentType: string } | null> {
|
): Promise<{ data: Uint8Array; contentType: string } | null> {
|
||||||
@@ -94,14 +123,12 @@ export async function GET(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
return NextResponse.json(
|
return redirectToTarget(target);
|
||||||
{ ok: false, error: `fetch image failed: HTTP ${resp.status}` },
|
|
||||||
{ status: 502 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = (resp.headers.get("content-type") ?? "").toLowerCase();
|
const headerType = (resp.headers.get("content-type") ?? "").toLowerCase();
|
||||||
if (!contentType.startsWith("image/")) {
|
const contentType = inferImageType(target, headerType);
|
||||||
|
if (!looksLikeImage(target, contentType)) {
|
||||||
return NextResponse.json({ ok: false, error: "url is not an image" }, { status: 400 });
|
return NextResponse.json({ ok: false, error: "url is not an image" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +152,8 @@ export async function GET(req: NextRequest) {
|
|||||||
"Cache-Control": "public, max-age=31536000, immutable",
|
"Cache-Control": "public, max-age=31536000, immutable",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch {
|
||||||
return NextResponse.json(
|
return redirectToTarget(target);
|
||||||
{ ok: false, error: `fetch image failed: ${String(e)}` },
|
|
||||||
{ status: 502 }
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,20 @@ type TriggerMissingResp = {
|
|||||||
max_solutions: number;
|
max_solutions: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AdminUser = {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
rating: number;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdminUsersResp = {
|
||||||
|
items: AdminUser[];
|
||||||
|
total_count: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
};
|
||||||
|
|
||||||
function fmtTs(v: number | null | undefined): string {
|
function fmtTs(v: number | null | undefined): string {
|
||||||
if (!v) return "-";
|
if (!v) return "-";
|
||||||
return new Date(v * 1000).toLocaleString();
|
return new Date(v * 1000).toLocaleString();
|
||||||
@@ -81,17 +95,24 @@ export default function BackendLogsPage() {
|
|||||||
const [queuedIds, setQueuedIds] = useState<number[]>([]);
|
const [queuedIds, setQueuedIds] = useState<number[]>([]);
|
||||||
const [triggerLoading, setTriggerLoading] = useState(false);
|
const [triggerLoading, setTriggerLoading] = useState(false);
|
||||||
const [triggerMsg, setTriggerMsg] = useState("");
|
const [triggerMsg, setTriggerMsg] = useState("");
|
||||||
|
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||||
|
const [deleteUserId, setDeleteUserId] = useState<number | null>(null);
|
||||||
|
const [userMsg, setUserMsg] = useState("");
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (!isAdmin || !token) return;
|
if (!isAdmin || !token) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch<BackendLogsResp>(
|
const [data, usersData] = await Promise.all([
|
||||||
`/api/v1/backend/logs?limit=${limit}&running_limit=20&queued_limit=100`,
|
apiFetch<BackendLogsResp>(
|
||||||
{},
|
`/api/v1/backend/logs?limit=${limit}&running_limit=20&queued_limit=100`,
|
||||||
token
|
{},
|
||||||
);
|
token
|
||||||
|
),
|
||||||
|
apiFetch<AdminUsersResp>("/api/v1/admin/users?page=1&page_size=200", {}, token),
|
||||||
|
]);
|
||||||
|
|
||||||
setPendingJobs(data.pending_jobs ?? 0);
|
setPendingJobs(data.pending_jobs ?? 0);
|
||||||
setMissingProblems(data.missing_problems ?? 0);
|
setMissingProblems(data.missing_problems ?? 0);
|
||||||
setItems(data.items ?? []);
|
setItems(data.items ?? []);
|
||||||
@@ -99,6 +120,7 @@ export default function BackendLogsPage() {
|
|||||||
setQueuedJobs(data.queued_jobs ?? []);
|
setQueuedJobs(data.queued_jobs ?? []);
|
||||||
setRunningIds(data.running_problem_ids ?? []);
|
setRunningIds(data.running_problem_ids ?? []);
|
||||||
setQueuedIds(data.queued_problem_ids ?? []);
|
setQueuedIds(data.queued_problem_ids ?? []);
|
||||||
|
setUsers(usersData.items ?? []);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -185,6 +207,45 @@ export default function BackendLogsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (user: AdminUser) => {
|
||||||
|
if (!isAdmin || !token) return;
|
||||||
|
if (user.username === "admin") {
|
||||||
|
setError(tx("保留管理员账号不可删除", "Reserved admin account cannot be deleted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = window.confirm(
|
||||||
|
tx(
|
||||||
|
`确认删除用户 ${user.username}(#${user.id})?该用户提交记录、错题本、草稿、积分记录会被级联删除。`,
|
||||||
|
`Delete user ${user.username}(#${user.id})? Submissions, wrong-book, drafts, and points records will be removed by cascade.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
setDeleteUserId(user.id);
|
||||||
|
setError("");
|
||||||
|
setUserMsg("");
|
||||||
|
try {
|
||||||
|
await apiFetch(
|
||||||
|
`/api/v1/admin/users/${user.id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
setUserMsg(
|
||||||
|
tx(
|
||||||
|
`已删除用户 ${user.username}(#${user.id})`,
|
||||||
|
`Deleted user ${user.username}(#${user.id}).`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await refresh();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setError(String(e));
|
||||||
|
} finally {
|
||||||
|
setDeleteUserId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAdmin || !token) return;
|
if (!isAdmin || !token) return;
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -250,12 +311,94 @@ export default function BackendLogsPage() {
|
|||||||
|
|
||||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||||
{triggerMsg && <p className="mt-3 text-sm text-emerald-700">{triggerMsg}</p>}
|
{triggerMsg && <p className="mt-3 text-sm text-emerald-700">{triggerMsg}</p>}
|
||||||
|
{userMsg && <p className="mt-3 text-sm text-emerald-700">{userMsg}</p>}
|
||||||
<p className="mt-3 text-xs text-zinc-500">
|
<p className="mt-3 text-xs text-zinc-500">
|
||||||
{tx(
|
{tx(
|
||||||
"系统已自动单线程异步处理待队列任务,无需手工点击;上方按钮仅用于立即手动补全。",
|
"系统已自动单线程异步处理待队列任务,无需手工点击;上方按钮仅用于立即手动补全。",
|
||||||
"System auto-processes queued jobs in single-thread async mode; the button above is only for manual trigger."
|
"System auto-processes queued jobs in single-thread async mode; the button above is only for manual trigger."
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<section className="mt-4 rounded-xl border bg-white p-3">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<h2 className="text-sm font-medium">{tx("用户管理(可删除)", "User Management (Delete Supported)")}</h2>
|
||||||
|
<p className="text-xs text-zinc-600">
|
||||||
|
{tx("总用户", "Total users")} {users.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 divide-y md:hidden">
|
||||||
|
{users.map((user) => (
|
||||||
|
<article key={user.id} className="space-y-1 py-2 text-xs">
|
||||||
|
<p className="font-medium">
|
||||||
|
#{user.id} · {user.username}
|
||||||
|
</p>
|
||||||
|
<p className="text-zinc-600">
|
||||||
|
Rating {user.rating} · {tx("创建", "Created")} {fmtTs(user.created_at)}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="rounded border px-2 py-1 text-xs text-red-700 hover:bg-red-50 disabled:opacity-60"
|
||||||
|
disabled={deleteUserId === user.id || user.username === "admin"}
|
||||||
|
onClick={() => void deleteUser(user)}
|
||||||
|
>
|
||||||
|
{user.username === "admin"
|
||||||
|
? tx("保留账号", "Reserved")
|
||||||
|
: deleteUserId === user.id
|
||||||
|
? tx("删除中...", "Deleting...")
|
||||||
|
: tx("删除用户", "Delete User")}
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
{!loading && users.length === 0 && (
|
||||||
|
<p className="py-4 text-center text-sm text-zinc-500">{tx("暂无用户数据", "No users found")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 hidden overflow-x-auto md:block">
|
||||||
|
<table className="min-w-full text-xs">
|
||||||
|
<thead className="bg-zinc-100 text-left">
|
||||||
|
<tr>
|
||||||
|
<th className="px-2 py-2">ID</th>
|
||||||
|
<th className="px-2 py-2">{tx("用户名", "Username")}</th>
|
||||||
|
<th className="px-2 py-2">Rating</th>
|
||||||
|
<th className="px-2 py-2">{tx("创建时间", "Created At")}</th>
|
||||||
|
<th className="px-2 py-2">{tx("操作", "Action")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => (
|
||||||
|
<tr key={user.id} className="border-t">
|
||||||
|
<td className="px-2 py-2">{user.id}</td>
|
||||||
|
<td className="px-2 py-2">{user.username}</td>
|
||||||
|
<td className="px-2 py-2">{user.rating}</td>
|
||||||
|
<td className="px-2 py-2 text-zinc-600">{fmtTs(user.created_at)}</td>
|
||||||
|
<td className="px-2 py-2">
|
||||||
|
<button
|
||||||
|
className="rounded border px-2 py-1 text-xs text-red-700 hover:bg-red-50 disabled:opacity-60"
|
||||||
|
disabled={deleteUserId === user.id || user.username === "admin"}
|
||||||
|
onClick={() => void deleteUser(user)}
|
||||||
|
>
|
||||||
|
{user.username === "admin"
|
||||||
|
? tx("保留账号", "Reserved")
|
||||||
|
: deleteUserId === user.id
|
||||||
|
? tx("删除中...", "Deleting...")
|
||||||
|
: tx("删除用户", "Delete User")}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!loading && users.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="px-2 py-4 text-center text-zinc-500" colSpan={5}>
|
||||||
|
{tx("暂无用户数据", "No users found")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="mt-4 grid gap-3 md:grid-cols-2">
|
<section className="mt-4 grid gap-3 md:grid-cols-2">
|
||||||
<article className="rounded-xl border bg-white p-3">
|
<article className="rounded-xl border bg-white p-3">
|
||||||
<h2 className="text-sm font-medium">{tx("正在处理(Running)", "Running Jobs")}</h2>
|
<h2 className="text-sm font-medium">{tx("正在处理(Running)", "Running Jobs")}</h2>
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ body {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.solution-code-block {
|
||||||
|
font-size: calc(0.875rem * 0.7) !important;
|
||||||
|
line-height: 1.05rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.print-only {
|
.print-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { PixelAvatar } from "@/components/pixel-avatar";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { readToken } from "@/lib/auth";
|
import { readToken } from "@/lib/auth";
|
||||||
import { useI18nText } from "@/lib/i18n";
|
import { useI18nText } from "@/lib/i18n";
|
||||||
@@ -218,10 +219,23 @@ export default function MePage() {
|
|||||||
|
|
||||||
{profile && (
|
{profile && (
|
||||||
<section className="mt-4 rounded-xl border bg-white p-4 text-sm">
|
<section className="mt-4 rounded-xl border bg-white p-4 text-sm">
|
||||||
<p>ID: {profile.id}</p>
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<p>{tx("用户名", "Username")}: {profile.username}</p>
|
<PixelAvatar
|
||||||
<p>Rating: {profile.rating}</p>
|
seed={`${profile.username}-${profile.id}`}
|
||||||
<p>{tx("创建时间", "Created At")}: {fmtTs(profile.created_at)}</p>
|
size={72}
|
||||||
|
className="border-zinc-700"
|
||||||
|
alt={`${profile.username} avatar`}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>ID: {profile.id}</p>
|
||||||
|
<p>{tx("用户名", "Username")}: {profile.username}</p>
|
||||||
|
<p>Rating: {profile.rating}</p>
|
||||||
|
<p>{tx("创建时间", "Created At")}: {fmtTs(profile.created_at)}</p>
|
||||||
|
<p className="text-xs text-zinc-500">
|
||||||
|
{tx("默认像素头像按账号随机生成,可作为主题角色形象。", "Default pixel avatar is randomly generated by account as your theme character.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { CodeEditor } from "@/components/code-editor";
|
import { CodeEditor } from "@/components/code-editor";
|
||||||
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
import { MarkdownRenderer } from "@/components/markdown-renderer";
|
||||||
@@ -100,6 +100,12 @@ function scoreRatio(score: number): number {
|
|||||||
return Math.max(0, Math.min(100, score));
|
return Math.max(0, Math.min(100, score));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDraftSignature(code: string, stdinText: string): string {
|
||||||
|
const normCode = normalizeCodeText(code);
|
||||||
|
const normStdin = (stdinText ?? "").replace(/\r\n?/g, "\n");
|
||||||
|
return `${normCode}\n__STDIN__\n${normStdin}`;
|
||||||
|
}
|
||||||
|
|
||||||
type ResultTone = {
|
type ResultTone = {
|
||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -154,7 +160,8 @@ type DraftResp = {
|
|||||||
language: string;
|
language: string;
|
||||||
code: string;
|
code: string;
|
||||||
stdin: string;
|
stdin: string;
|
||||||
updated_at: number;
|
updated_at: number | null;
|
||||||
|
exists?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SolutionItem = {
|
type SolutionItem = {
|
||||||
@@ -292,6 +299,14 @@ export default function ProblemDetailPage() {
|
|||||||
const [solutionData, setSolutionData] = useState<SolutionResp | null>(null);
|
const [solutionData, setSolutionData] = useState<SolutionResp | null>(null);
|
||||||
const [solutionMsg, setSolutionMsg] = useState("");
|
const [solutionMsg, setSolutionMsg] = useState("");
|
||||||
const [printAnswerMarkdown, setPrintAnswerMarkdown] = useState("");
|
const [printAnswerMarkdown, setPrintAnswerMarkdown] = useState("");
|
||||||
|
const draftLatestRef = useRef<{ code: string; stdin: string }>({
|
||||||
|
code: starterCode,
|
||||||
|
stdin: defaultRunInput,
|
||||||
|
});
|
||||||
|
const draftLastSavedSigRef = useRef<string>(
|
||||||
|
buildDraftSignature(starterCode, defaultRunInput)
|
||||||
|
);
|
||||||
|
const draftAutoSavingRef = useRef(false);
|
||||||
|
|
||||||
const llmProfile = useMemo<LlmProfile | null>(() => {
|
const llmProfile = useMemo<LlmProfile | null>(() => {
|
||||||
if (!problem?.llm_profile_json) return null;
|
if (!problem?.llm_profile_json) return null;
|
||||||
@@ -392,8 +407,14 @@ export default function ProblemDetailPage() {
|
|||||||
setShowSolutions(false);
|
setShowSolutions(false);
|
||||||
setSolutionData(null);
|
setSolutionData(null);
|
||||||
setSolutionMsg("");
|
setSolutionMsg("");
|
||||||
|
draftLatestRef.current = { code: starterCode, stdin: defaultRunInput };
|
||||||
|
draftLastSavedSigRef.current = buildDraftSignature(starterCode, defaultRunInput);
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
draftLatestRef.current = { code, stdin: runInput };
|
||||||
|
}, [code, runInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clearPrintCache = () => setPrintAnswerMarkdown("");
|
const clearPrintCache = () => setPrintAnswerMarkdown("");
|
||||||
window.addEventListener("afterprint", clearPrintCache);
|
window.addEventListener("afterprint", clearPrintCache);
|
||||||
@@ -477,9 +498,21 @@ export default function ProblemDetailPage() {
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const draft = await apiFetch<DraftResp>(`/api/v1/problems/${id}/draft`, undefined, token);
|
const draft = await apiFetch<DraftResp>(`/api/v1/problems/${id}/draft`, undefined, token);
|
||||||
if (draft.code) setCode(draft.code);
|
const hasDraft = Boolean(draft.exists) || Boolean(draft.code) || Boolean(draft.stdin);
|
||||||
if (draft.stdin) setRunInput(draft.stdin);
|
let nextCode = draftLatestRef.current.code;
|
||||||
setDraftMsg(tx("已自动加载草稿", "Draft auto-loaded"));
|
let nextStdin = draftLatestRef.current.stdin;
|
||||||
|
if (draft.code) {
|
||||||
|
nextCode = draft.code;
|
||||||
|
setCode(draft.code);
|
||||||
|
}
|
||||||
|
if (typeof draft.stdin === "string" && draft.stdin.length > 0) {
|
||||||
|
nextStdin = draft.stdin;
|
||||||
|
setRunInput(draft.stdin);
|
||||||
|
}
|
||||||
|
draftLastSavedSigRef.current = buildDraftSignature(nextCode, nextStdin);
|
||||||
|
if (hasDraft) {
|
||||||
|
setDraftMsg(tx("已自动加载草稿", "Draft auto-loaded"));
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore empty draft / unauthorized
|
// ignore empty draft / unauthorized
|
||||||
}
|
}
|
||||||
@@ -487,6 +520,39 @@ export default function ProblemDetailPage() {
|
|||||||
void loadDraft();
|
void loadDraft();
|
||||||
}, [id, tx]);
|
}, [id, tx]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return;
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
const token = readToken();
|
||||||
|
if (!token || draftAutoSavingRef.current) return;
|
||||||
|
|
||||||
|
const payload = draftLatestRef.current;
|
||||||
|
const nextSig = buildDraftSignature(payload.code, payload.stdin);
|
||||||
|
if (nextSig === draftLastSavedSigRef.current) return;
|
||||||
|
|
||||||
|
draftAutoSavingRef.current = true;
|
||||||
|
void apiFetch<{ saved: boolean }>(
|
||||||
|
`/api/v1/problems/${id}/draft`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ language: "cpp", code: payload.code, stdin: payload.stdin }),
|
||||||
|
},
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
draftLastSavedSigRef.current = nextSig;
|
||||||
|
setDraftMsg(tx("草稿已自动保存(每60秒)", "Draft auto-saved (every 60s)"));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Keep silent to avoid noisy notifications on transient network errors.
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
draftAutoSavingRef.current = false;
|
||||||
|
});
|
||||||
|
}, 60000);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [id, tx]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!problemId) return;
|
if (!problemId) return;
|
||||||
setRunInput((prev) => (prev.trim().length > 0 ? prev : sampleInput || defaultRunInput));
|
setRunInput((prev) => (prev.trim().length > 0 ? prev : sampleInput || defaultRunInput));
|
||||||
@@ -555,6 +621,7 @@ export default function ProblemDetailPage() {
|
|||||||
},
|
},
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
draftLastSavedSigRef.current = buildDraftSignature(code, runInput);
|
||||||
setDraftMsg(tx("草稿已保存", "Draft saved"));
|
setDraftMsg(tx("草稿已保存", "Draft saved"));
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
setError(String(e));
|
setError(String(e));
|
||||||
@@ -1084,7 +1151,7 @@ export default function ProblemDetailPage() {
|
|||||||
{tx("写入并试运行", "Insert & Run")}
|
{tx("写入并试运行", "Insert & Run")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="overflow-x-auto whitespace-pre p-3 text-sm leading-6 text-zinc-100">
|
<pre className="solution-code-block overflow-x-auto whitespace-pre p-3 text-sm leading-6 text-zinc-100">
|
||||||
{normalizeCodeText(item.code_cpp)}
|
{normalizeCodeText(item.code_cpp)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { PixelAvatar } from "@/components/pixel-avatar";
|
||||||
import { useUiPreferences } from "@/components/ui-preference-provider";
|
import { useUiPreferences } from "@/components/ui-preference-provider";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { clearToken, readToken } from "@/lib/auth";
|
import { clearToken, readToken } from "@/lib/auth";
|
||||||
@@ -19,6 +20,11 @@ type NavGroup = {
|
|||||||
links: NavLink[];
|
links: NavLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MeProfile = {
|
||||||
|
id?: number;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function buildNavGroups(t: (key: string) => string, isAdmin: boolean): NavGroup[] {
|
function buildNavGroups(t: (key: string) => string, isAdmin: boolean): NavGroup[] {
|
||||||
const groups: NavGroup[] = [
|
const groups: NavGroup[] = [
|
||||||
{
|
{
|
||||||
@@ -97,12 +103,13 @@ export function AppNav() {
|
|||||||
|
|
||||||
const [hasToken, setHasToken] = useState<boolean>(() => Boolean(readToken()));
|
const [hasToken, setHasToken] = useState<boolean>(() => Boolean(readToken()));
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const [meProfile, setMeProfile] = useState<MeProfile | null>(null);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [desktopOpenGroup, setDesktopOpenGroup] = useState<string | null>(null);
|
const [desktopOpenGroup, setDesktopOpenGroup] = useState<string | null>(null);
|
||||||
const desktopMenuRef = useRef<HTMLDivElement | null>(null);
|
const desktopMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const navGroups = useMemo(() => buildNavGroups(t, isAdmin), [isAdmin, t]);
|
const navGroups = useMemo(() => buildNavGroups(t, isAdmin), [isAdmin, t]);
|
||||||
const activeGroup = resolveActiveGroup(pathname, navGroups);
|
const activeGroup = resolveActiveGroup(pathname, navGroups);
|
||||||
const usePopupSecondary = theme === "default";
|
const usePopupSecondary = theme === "default" || theme === "minecraft";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
@@ -112,15 +119,20 @@ export function AppNav() {
|
|||||||
setHasToken(Boolean(token));
|
setHasToken(Boolean(token));
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setIsAdmin(false);
|
setIsAdmin(false);
|
||||||
|
setMeProfile(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const me = await apiFetch<{ username?: string }>("/api/v1/me", {}, token);
|
const me = await apiFetch<MeProfile>("/api/v1/me", {}, token);
|
||||||
if (!canceled) {
|
if (!canceled) {
|
||||||
setIsAdmin((me?.username ?? "") === "admin");
|
setIsAdmin((me?.username ?? "") === "admin");
|
||||||
|
setMeProfile(me ?? null);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (!canceled) setIsAdmin(false);
|
if (!canceled) {
|
||||||
|
setIsAdmin(false);
|
||||||
|
setMeProfile(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
@@ -149,6 +161,16 @@ export function AppNav() {
|
|||||||
}, [desktopOpenGroup, usePopupSecondary]);
|
}, [desktopOpenGroup, usePopupSecondary]);
|
||||||
|
|
||||||
const currentGroup = navGroups.find((g) => g.key === activeGroup) ?? navGroups[0];
|
const currentGroup = navGroups.find((g) => g.key === activeGroup) ?? navGroups[0];
|
||||||
|
const avatarSeed = meProfile
|
||||||
|
? `${meProfile.username ?? "user"}-${meProfile.id ?? ""}`
|
||||||
|
: "guest";
|
||||||
|
const handleLogout = () => {
|
||||||
|
clearToken();
|
||||||
|
setHasToken(false);
|
||||||
|
setIsAdmin(false);
|
||||||
|
setMeProfile(null);
|
||||||
|
setDesktopOpenGroup(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="print-hidden border-b bg-[color:var(--surface)]/95 backdrop-blur supports-[backdrop-filter]:bg-[color:var(--surface)]/85">
|
<header className="print-hidden border-b bg-[color:var(--surface)]/95 backdrop-blur supports-[backdrop-filter]:bg-[color:var(--surface)]/85">
|
||||||
@@ -188,7 +210,11 @@ export function AppNav() {
|
|||||||
{group.label}
|
{group.label}
|
||||||
</button>
|
</button>
|
||||||
{opened && (
|
{opened && (
|
||||||
<div className="absolute left-0 top-full z-50 mt-2 min-w-[11rem] rounded-md border bg-[color:var(--surface)] p-1 shadow-lg">
|
<div
|
||||||
|
className={`absolute left-0 top-full z-50 mt-2 rounded-md border bg-[color:var(--surface)] p-1 shadow-lg ${
|
||||||
|
group.key === "account" ? "min-w-[18rem]" : "min-w-[11rem]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{group.links.map((item) => {
|
{group.links.map((item) => {
|
||||||
const linkActive = isActivePath(pathname, item.href);
|
const linkActive = isActivePath(pathname, item.href);
|
||||||
return (
|
return (
|
||||||
@@ -207,6 +233,64 @@ export function AppNav() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{group.key === "account" && (
|
||||||
|
<div className="mt-2 space-y-2 border-t border-zinc-200 px-2 pt-2">
|
||||||
|
<label className="block text-xs">
|
||||||
|
<span className="text-zinc-500">{t("prefs.theme")}</span>
|
||||||
|
<select
|
||||||
|
className="mt-1 w-full rounded-md border px-2 py-1 text-xs"
|
||||||
|
value={theme}
|
||||||
|
onChange={(e) => setTheme(e.target.value as ThemeId)}
|
||||||
|
>
|
||||||
|
{themes.map((item) => (
|
||||||
|
<option key={item.id} value={item.id}>
|
||||||
|
{item.labels[language]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="block text-xs">
|
||||||
|
<span className="text-zinc-500">{t("prefs.language")}</span>
|
||||||
|
<select
|
||||||
|
className="mt-1 w-full rounded-md border px-2 py-1 text-xs"
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value === "zh" ? "zh" : "en")}
|
||||||
|
>
|
||||||
|
<option value="en">{t("prefs.lang.en")}</option>
|
||||||
|
<option value="zh">{t("prefs.lang.zh")}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-md border border-zinc-200 px-2 py-1.5">
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<span className={hasToken ? "text-emerald-700" : "text-zinc-500"}>
|
||||||
|
{hasToken ? t("nav.logged_in") : t("nav.logged_out")}
|
||||||
|
</span>
|
||||||
|
{hasToken && (
|
||||||
|
<>
|
||||||
|
<PixelAvatar
|
||||||
|
seed={avatarSeed}
|
||||||
|
size={20}
|
||||||
|
className="border-zinc-700"
|
||||||
|
alt={meProfile?.username ? `${meProfile.username} avatar` : "avatar"}
|
||||||
|
/>
|
||||||
|
{meProfile?.username && (
|
||||||
|
<span className="truncate text-xs text-zinc-500">{meProfile.username}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hasToken && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border px-2 py-1 text-xs hover:bg-zinc-100"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
{t("nav.logout")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +358,7 @@ export function AppNav() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-wrap items-center justify-end gap-2 text-xs sm:text-sm">
|
<div className={`mt-2 flex flex-wrap items-center justify-end gap-2 text-xs sm:text-sm ${usePopupSecondary ? "md:hidden" : ""}`}>
|
||||||
<label className="inline-flex items-center gap-1">
|
<label className="inline-flex items-center gap-1">
|
||||||
<span className="text-zinc-500">{t("prefs.theme")}</span>
|
<span className="text-zinc-500">{t("prefs.theme")}</span>
|
||||||
<select
|
<select
|
||||||
@@ -306,13 +390,17 @@ export function AppNav() {
|
|||||||
<span className={hasToken ? "text-emerald-700" : "text-zinc-500"}>
|
<span className={hasToken ? "text-emerald-700" : "text-zinc-500"}>
|
||||||
{hasToken ? t("nav.logged_in") : t("nav.logged_out")}
|
{hasToken ? t("nav.logged_in") : t("nav.logged_out")}
|
||||||
</span>
|
</span>
|
||||||
|
{hasToken && (
|
||||||
|
<PixelAvatar
|
||||||
|
seed={avatarSeed}
|
||||||
|
size={24}
|
||||||
|
className="border-zinc-700"
|
||||||
|
alt={meProfile?.username ? `${meProfile.username} avatar` : "avatar"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{hasToken && (
|
{hasToken && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleLogout}
|
||||||
clearToken();
|
|
||||||
setHasToken(false);
|
|
||||||
setIsAdmin(false);
|
|
||||||
}}
|
|
||||||
className="rounded-md border px-3 py-1 hover:bg-zinc-100"
|
className="rounded-md border px-3 py-1 hover:bg-zinc-100"
|
||||||
>
|
>
|
||||||
{t("nav.logout")}
|
{t("nav.logout")}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import dynamic from "next/dynamic";
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import type { editor as MonacoEditorNS, IDisposable, MarkerSeverity, IPosition } from "monaco-editor";
|
import type { editor as MonacoEditorNS, IDisposable, MarkerSeverity, IPosition } from "monaco-editor";
|
||||||
|
|
||||||
|
import { useUiPreferences } from "@/components/ui-preference-provider";
|
||||||
import { analyzeCpp14Policy, type Cpp14PolicyIssue } from "@/lib/cpp14-policy";
|
import { analyzeCpp14Policy, type Cpp14PolicyIssue } from "@/lib/cpp14-policy";
|
||||||
import { useI18nText } from "@/lib/i18n";
|
import { useI18nText } from "@/lib/i18n";
|
||||||
|
|
||||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||||
const POLICY_MARKER_OWNER = "csp-cpp14-policy";
|
const POLICY_MARKER_OWNER = "csp-cpp14-policy";
|
||||||
|
const MINECRAFT_MONACO_THEME = "csp-minecraft-dark";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -94,6 +96,40 @@ function localizePolicyIssue(
|
|||||||
return { ...issue, ...localized };
|
return { ...issue, ...localized };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defineMinecraftMonacoTheme(monaco: typeof import("monaco-editor")): void {
|
||||||
|
monaco.editor.defineTheme(MINECRAFT_MONACO_THEME, {
|
||||||
|
base: "vs-dark",
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: "", foreground: "ECECEC", background: "111317" },
|
||||||
|
{ token: "comment", foreground: "7F8A99", fontStyle: "italic" },
|
||||||
|
{ token: "keyword", foreground: "C792EA" },
|
||||||
|
{ token: "type", foreground: "82AAFF" },
|
||||||
|
{ token: "number", foreground: "F78C6C" },
|
||||||
|
{ token: "string", foreground: "C3E88D" },
|
||||||
|
{ token: "operator", foreground: "89DDFF" },
|
||||||
|
{ token: "delimiter.bracket", foreground: "D8DEE9" },
|
||||||
|
{ token: "predefined", foreground: "FFD580" },
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#111317",
|
||||||
|
"editor.foreground": "#ECECEC",
|
||||||
|
"editorCursor.foreground": "#FFE082",
|
||||||
|
"editorLineNumber.foreground": "#768296",
|
||||||
|
"editorLineNumber.activeForeground": "#C7D0DE",
|
||||||
|
"editor.selectionBackground": "#2A3347",
|
||||||
|
"editor.inactiveSelectionBackground": "#232B3C",
|
||||||
|
"editor.lineHighlightBackground": "#171D26",
|
||||||
|
"editor.wordHighlightBackground": "#303A4E99",
|
||||||
|
"editorIndentGuide.background": "#2A3240",
|
||||||
|
"editorIndentGuide.activeBackground": "#3D485A",
|
||||||
|
"editorBracketMatch.background": "#2B364A",
|
||||||
|
"editorBracketMatch.border": "#89DDFF88",
|
||||||
|
"editorGutter.background": "#111317",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function CodeEditor({
|
export function CodeEditor({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -101,10 +137,12 @@ export function CodeEditor({
|
|||||||
fontSize = 14,
|
fontSize = 14,
|
||||||
onPolicyIssuesChange,
|
onPolicyIssuesChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { theme } = useUiPreferences();
|
||||||
const { tx } = useI18nText();
|
const { tx } = useI18nText();
|
||||||
const editorRef = useRef<MonacoEditorNS.IStandaloneCodeEditor | null>(null);
|
const editorRef = useRef<MonacoEditorNS.IStandaloneCodeEditor | null>(null);
|
||||||
const monacoRef = useRef<typeof import("monaco-editor") | null>(null);
|
const monacoRef = useRef<typeof import("monaco-editor") | null>(null);
|
||||||
const completionRef = useRef<IDisposable | null>(null);
|
const completionRef = useRef<IDisposable | null>(null);
|
||||||
|
const editorTheme = theme === "minecraft" ? MINECRAFT_MONACO_THEME : "vs";
|
||||||
|
|
||||||
const updatePolicyIssues = useCallback(
|
const updatePolicyIssues = useCallback(
|
||||||
(nextCode: string) => {
|
(nextCode: string) => {
|
||||||
@@ -145,10 +183,20 @@ export function CodeEditor({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const monaco = monacoRef.current;
|
||||||
|
if (!monaco) return;
|
||||||
|
if (editorTheme === MINECRAFT_MONACO_THEME) {
|
||||||
|
defineMinecraftMonacoTheme(monaco);
|
||||||
|
}
|
||||||
|
monaco.editor.setTheme(editorTheme);
|
||||||
|
}, [editorTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
height={height}
|
height={height}
|
||||||
language="cpp"
|
language="cpp"
|
||||||
|
theme={editorTheme}
|
||||||
value={value}
|
value={value}
|
||||||
options={{
|
options={{
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -167,6 +215,10 @@ export function CodeEditor({
|
|||||||
onMount={(editor, monaco) => {
|
onMount={(editor, monaco) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
monacoRef.current = monaco;
|
monacoRef.current = monaco;
|
||||||
|
if (editorTheme === MINECRAFT_MONACO_THEME) {
|
||||||
|
defineMinecraftMonacoTheme(monaco);
|
||||||
|
}
|
||||||
|
monaco.editor.setTheme(editorTheme);
|
||||||
|
|
||||||
if (!completionRef.current) {
|
if (!completionRef.current) {
|
||||||
completionRef.current = monaco.languages.registerCompletionItemProvider("cpp", {
|
completionRef.current = monaco.languages.registerCompletionItemProvider("cpp", {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { buildAvatarSeed, generatePixelAvatarDataUri } from "@/lib/pixel-avatar";
|
||||||
|
|
||||||
|
type PixelAvatarProps = {
|
||||||
|
seed: string;
|
||||||
|
size?: number;
|
||||||
|
className?: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PixelAvatar({
|
||||||
|
seed,
|
||||||
|
size = 40,
|
||||||
|
className = "",
|
||||||
|
alt = "pixel avatar",
|
||||||
|
}: PixelAvatarProps) {
|
||||||
|
const resolvedSeed = useMemo(() => buildAvatarSeed(seed), [seed]);
|
||||||
|
const src = useMemo(() => generatePixelAvatarDataUri(resolvedSeed), [resolvedSeed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`pixel-avatar-frame inline-flex items-center justify-center overflow-hidden rounded border ${className}`.trim()}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
unoptimized
|
||||||
|
className="pixel-avatar-image h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ export const API_BASE =
|
|||||||
(process.env.NODE_ENV === "development" ? "http://localhost:8080" : "/admin139");
|
(process.env.NODE_ENV === "development" ? "http://localhost:8080" : "/admin139");
|
||||||
|
|
||||||
function uiText(zhText: string, enText: string): string {
|
function uiText(zhText: string, enText: string): string {
|
||||||
if (typeof window === "undefined") return enText;
|
if (typeof window === "undefined") return zhText;
|
||||||
const lang = window.localStorage.getItem("csp.ui.language");
|
const lang = window.localStorage.getItem("csp.ui.language");
|
||||||
return lang === "zh" ? zhText : enText;
|
return lang === "en" ? enText : zhText;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiEnvelope<T> =
|
type ApiEnvelope<T> =
|
||||||
|
|||||||
@@ -186,21 +186,5 @@ export function analyzeCpp14Policy(code: string): Cpp14PolicyIssue[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/\bfreopen\s*\(/.test(text) && /\bint\s+main\s*\(/.test(text)) {
|
|
||||||
const idx = text.search(/\bint\s+main\s*\(/);
|
|
||||||
const pos = offsetToPosition(lineStarts, Math.max(0, idx));
|
|
||||||
pushIssue(
|
|
||||||
issues,
|
|
||||||
"freopen-tip",
|
|
||||||
"hint",
|
|
||||||
"未检测到 freopen(福建二轮常见文件读写要求)",
|
|
||||||
"若考场题面要求 *.in/*.out,请按官方文件名补上 freopen。",
|
|
||||||
pos.line,
|
|
||||||
pos.column,
|
|
||||||
pos.line,
|
|
||||||
pos.column + 3
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues;
|
return issues;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useUiPreferences } from "@/components/ui-preference-provider";
|
import { useUiPreferences } from "@/components/ui-preference-provider";
|
||||||
|
|
||||||
export function readUiLanguage(): "en" | "zh" {
|
export function readUiLanguage(): "en" | "zh" {
|
||||||
if (typeof window === "undefined") return "en";
|
if (typeof window === "undefined") return "zh";
|
||||||
return window.localStorage.getItem("csp.ui.language") === "zh" ? "zh" : "en";
|
return window.localStorage.getItem("csp.ui.language") === "en" ? "en" : "zh";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useI18nText() {
|
export function useI18nText() {
|
||||||
const { language } = useUiPreferences();
|
const { language } = useUiPreferences();
|
||||||
const isZh = language === "zh";
|
const isZh = language === "zh";
|
||||||
const tx = (zhText: string, enText: string) => (isZh ? zhText : enText);
|
const tx = useCallback(
|
||||||
|
(zhText: string, enText: string) => (isZh ? zhText : enText),
|
||||||
|
[isZh]
|
||||||
|
);
|
||||||
return { language, isZh, tx };
|
return { language, isZh, tx };
|
||||||
}
|
}
|
||||||
|
|||||||
74
frontend/src/lib/pixel-avatar.ts
普通文件
74
frontend/src/lib/pixel-avatar.ts
普通文件
@@ -0,0 +1,74 @@
|
|||||||
|
function hashSeed(seed: string): number {
|
||||||
|
let h = 2166136261 >>> 0;
|
||||||
|
for (let i = 0; i < seed.length; i += 1) {
|
||||||
|
h ^= seed.charCodeAt(i);
|
||||||
|
h = Math.imul(h, 16777619) >>> 0;
|
||||||
|
}
|
||||||
|
return h >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mulberry32(seed: number): () => number {
|
||||||
|
let t = seed >>> 0;
|
||||||
|
return () => {
|
||||||
|
t += 0x6d2b79f5;
|
||||||
|
let x = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
|
||||||
|
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKIN_PALETTE = ["#f6d3b3", "#e6be95", "#d7a980", "#f8dec6", "#c99264"];
|
||||||
|
const HAIR_PALETTE = ["#2f1b12", "#4a2f1d", "#6d4c41", "#212121", "#4e342e", "#1b5e20"];
|
||||||
|
const ACCENT_PALETTE = ["#7cb342", "#00b0d6", "#ffb300", "#8d6e63", "#ab47bc", "#ef5350"];
|
||||||
|
|
||||||
|
export function buildAvatarSeed(...parts: Array<string | number | null | undefined>): string {
|
||||||
|
const cleaned = parts
|
||||||
|
.map((part) => String(part ?? "").trim())
|
||||||
|
.filter((part) => part.length > 0);
|
||||||
|
return cleaned.length > 0 ? cleaned.join("|") : "guest|pixel";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePixelAvatarDataUri(seedInput: string, pixelSize = 10): string {
|
||||||
|
const seed = buildAvatarSeed(seedInput);
|
||||||
|
const rng = mulberry32(hashSeed(seed));
|
||||||
|
const skin = SKIN_PALETTE[Math.floor(rng() * SKIN_PALETTE.length)] ?? SKIN_PALETTE[0];
|
||||||
|
const hair = HAIR_PALETTE[Math.floor(rng() * HAIR_PALETTE.length)] ?? HAIR_PALETTE[0];
|
||||||
|
const accent = ACCENT_PALETTE[Math.floor(rng() * ACCENT_PALETTE.length)] ?? ACCENT_PALETTE[0];
|
||||||
|
const bg = rng() > 0.5 ? "#1f1f1f" : "#2b2b2b";
|
||||||
|
const border = rng() > 0.5 ? "#000000" : "#3e2723";
|
||||||
|
|
||||||
|
const width = 8 * pixelSize;
|
||||||
|
const height = 8 * pixelSize;
|
||||||
|
const rects: string[] = [
|
||||||
|
`<rect width="${width}" height="${height}" fill="${bg}"/>`,
|
||||||
|
`<rect x="0" y="0" width="${width}" height="${pixelSize}" fill="${accent}" opacity="0.4"/>`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let y = 0; y < 8; y += 1) {
|
||||||
|
for (let x = 0; x < 4; x += 1) {
|
||||||
|
const fillFace = y >= 1 && y <= 6 && x >= 1;
|
||||||
|
const threshold = fillFace ? 0.2 : 0.52;
|
||||||
|
if (rng() < threshold) continue;
|
||||||
|
|
||||||
|
const color = y <= 1 ? hair : y >= 6 ? accent : skin;
|
||||||
|
const leftX = x * pixelSize;
|
||||||
|
const rightX = (7 - x) * pixelSize;
|
||||||
|
const yPos = y * pixelSize;
|
||||||
|
rects.push(`<rect x="${leftX}" y="${yPos}" width="${pixelSize}" height="${pixelSize}" fill="${color}"/>`);
|
||||||
|
if (rightX !== leftX) {
|
||||||
|
rects.push(`<rect x="${rightX}" y="${yPos}" width="${pixelSize}" height="${pixelSize}" fill="${color}"/>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eyeY = 3 * pixelSize;
|
||||||
|
rects.push(`<rect x="${2 * pixelSize}" y="${eyeY}" width="${pixelSize}" height="${pixelSize}" fill="#111"/>`);
|
||||||
|
rects.push(`<rect x="${5 * pixelSize}" y="${eyeY}" width="${pixelSize}" height="${pixelSize}" fill="#111"/>`);
|
||||||
|
|
||||||
|
const mouthY = 5 * pixelSize;
|
||||||
|
rects.push(`<rect x="${3 * pixelSize}" y="${mouthY}" width="${2 * pixelSize}" height="${pixelSize}" fill="${hair}" opacity="0.8"/>`);
|
||||||
|
rects.push(`<rect x="${0}" y="${0}" width="${width}" height="${height}" fill="none" stroke="${border}" stroke-width="${Math.max(2, Math.floor(pixelSize / 4))}"/>`);
|
||||||
|
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">${rects.join("")}</svg>`;
|
||||||
|
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
@@ -247,6 +247,8 @@ labels: {
|
|||||||
来源参考:
|
来源参考:
|
||||||
|
|
||||||
- `ref/CSP-Minecraft-UI-Kit/docs/Design-Delivery-Document.html`
|
- `ref/CSP-Minecraft-UI-Kit/docs/Design-Delivery-Document.html`
|
||||||
|
- `ref/CSP-Minecraft-UI-Kit/html/index.html`
|
||||||
|
- `ref/CSP-Minecraft-UI-Kit/html/pages/*.html`
|
||||||
|
|
||||||
已落地核心规范:
|
已落地核心规范:
|
||||||
|
|
||||||
@@ -256,6 +258,7 @@ labels: {
|
|||||||
- 输入控件像素化风格。
|
- 输入控件像素化风格。
|
||||||
- 背景纹理与图片 `image-rendering: pixelated`。
|
- 背景纹理与图片 `image-rendering: pixelated`。
|
||||||
- 标题字体与文本阴影风格。
|
- 标题字体与文本阴影风格。
|
||||||
|
- 像素默认头像(按账号随机种子生成)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -291,4 +294,3 @@ labels: {
|
|||||||
- 将“按钮、卡片、输入框、徽章”收敛为主题化组件(而非纯 class 覆盖)。
|
- 将“按钮、卡片、输入框、徽章”收敛为主题化组件(而非纯 class 覆盖)。
|
||||||
- 增加主题视觉回归截图(关键页面自动快照)。
|
- 增加主题视觉回归截图(关键页面自动快照)。
|
||||||
- 建立对比度检测脚本,避免低对比度文本上线。
|
- 建立对比度检测脚本,避免低对比度文本上线。
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "DelaGothicOne";
|
font-family: "PressStart2P";
|
||||||
src: url("https://assets-persist.lovart.ai/agent-static-assets/DelaGothicOne-Regular.ttf");
|
src: url("https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2")
|
||||||
|
format("woff2");
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "MiSans";
|
font-family: "VT323";
|
||||||
src: url("https://assets-persist.lovart.ai/agent-static-assets/MiSans-Regular.ttf");
|
src: url("https://fonts.gstatic.com/s/vt323/v17/pxiKyp0ihIEF2hsY.woff2") format("woff2");
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "MiSansBold";
|
|
||||||
src: url("https://assets-persist.lovart.ai/agent-static-assets/MiSans-Bold.ttf");
|
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] {
|
:root[data-theme="minecraft"] {
|
||||||
--mc-grass-top: #5cb85c;
|
--mc-grass-top: #7cb342;
|
||||||
--mc-grass-side: #4cae4c;
|
--mc-grass-dark: #558b2f;
|
||||||
--mc-dirt: #795548;
|
--mc-dirt: #795548;
|
||||||
|
--mc-wood-dark: #5d4037;
|
||||||
|
--mc-wood: #8d6e63;
|
||||||
|
--mc-plank: #c69c6d;
|
||||||
|
--mc-plank-light: #efebe9;
|
||||||
--mc-stone: #9e9e9e;
|
--mc-stone: #9e9e9e;
|
||||||
--mc-stone-dark: #616161;
|
--mc-stone-dark: #616161;
|
||||||
--mc-obsidian: #212121;
|
--mc-obsidian: #1f1f1f;
|
||||||
--mc-wood: #8d6e63;
|
--mc-diamond: #00b0d6;
|
||||||
--mc-wood-dark: #5d4037;
|
--mc-gold: #ffb300;
|
||||||
--mc-gold: #ffd700;
|
--mc-red: #ef5350;
|
||||||
--mc-diamond: #40e0d0;
|
--mc-shadow: #000000;
|
||||||
--mc-redstone: #f44336;
|
|
||||||
|
|
||||||
--background: #1a1a1a;
|
--background: #161616;
|
||||||
--foreground: #f5f5f5;
|
--foreground: #f5f5f5;
|
||||||
--surface: #2d2d2d;
|
--surface: #262626;
|
||||||
--surface-soft: #242424;
|
--surface-soft: #1f1f1f;
|
||||||
--border: #000000;
|
--border: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] body {
|
:root[data-theme="minecraft"] body {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(rgba(26, 26, 26, 0.9), rgba(26, 26, 26, 0.9)),
|
linear-gradient(45deg, #1f1f1f 25%, transparent 25%),
|
||||||
url("https://a.lovart.ai/artifacts/agent/W1iXxVdg3xIm5fP9.png");
|
linear-gradient(-45deg, #1f1f1f 25%, transparent 25%),
|
||||||
background-size: 320px 320px;
|
linear-gradient(45deg, transparent 75%, #1f1f1f 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #1f1f1f 75%);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
background-position: 0 0, 0 20px, 20px -20px, -20px 0;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: "MiSans", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
font-family: "VT323", "MiSans", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
font-size: 1.06rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] h1,
|
:root[data-theme="minecraft"] h1,
|
||||||
@@ -52,9 +54,9 @@
|
|||||||
:root[data-theme="minecraft"] h4,
|
:root[data-theme="minecraft"] h4,
|
||||||
:root[data-theme="minecraft"] h5,
|
:root[data-theme="minecraft"] h5,
|
||||||
:root[data-theme="minecraft"] h6 {
|
:root[data-theme="minecraft"] h6 {
|
||||||
color: #ffffff;
|
font-family: "PressStart2P", "VT323", sans-serif;
|
||||||
font-family: "DelaGothicOne", "MiSansBold", "MiSans", sans-serif;
|
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
|
line-height: 1.5;
|
||||||
text-shadow: 2px 2px 0 #000000;
|
text-shadow: 2px 2px 0 #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,19 +67,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] ::-webkit-scrollbar-thumb {
|
:root[data-theme="minecraft"] ::-webkit-scrollbar-thumb {
|
||||||
background: var(--mc-stone);
|
background: var(--mc-wood);
|
||||||
border: 2px solid var(--mc-obsidian);
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .print-hidden {
|
||||||
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] header.print-hidden {
|
:root[data-theme="minecraft"] header.print-hidden {
|
||||||
background: rgba(33, 33, 33, 0.96) !important;
|
background: linear-gradient(180deg, #1d1d1d 0%, #2b2b2b 100%) !important;
|
||||||
border-bottom: 4px solid var(--mc-stone-dark);
|
border-bottom: 4px solid #000 !important;
|
||||||
box-shadow: 0 4px 0 rgba(0, 0, 0, 0.35);
|
box-shadow: 0 6px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] header.print-hidden::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: linear-gradient(180deg, var(--mc-grass-top) 0%, var(--mc-grass-dark) 100%);
|
||||||
|
border-bottom: 2px solid #2e7d32;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] nav.print-hidden.fixed {
|
:root[data-theme="minecraft"] nav.print-hidden.fixed {
|
||||||
background: rgba(33, 33, 33, 0.96) !important;
|
background: linear-gradient(180deg, #1d1d1d 0%, #2b2b2b 100%) !important;
|
||||||
border-top: 4px solid var(--mc-stone-dark);
|
border-top: 4px solid #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .rounded-xl.border,
|
:root[data-theme="minecraft"] .rounded-xl.border,
|
||||||
@@ -85,28 +103,39 @@
|
|||||||
:root[data-theme="minecraft"] .rounded-md.border,
|
:root[data-theme="minecraft"] .rounded-md.border,
|
||||||
:root[data-theme="minecraft"] .rounded.border {
|
:root[data-theme="minecraft"] .rounded.border {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
border-color: #000000 !important;
|
border: 3px solid #000 !important;
|
||||||
border-width: 3px !important;
|
box-shadow:
|
||||||
box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.45);
|
5px 5px 0 rgba(0, 0, 0, 0.48),
|
||||||
|
inset 2px 2px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] main section.rounded-xl.border.bg-white,
|
||||||
|
:root[data-theme="minecraft"] main article.rounded-xl.border.bg-white {
|
||||||
|
background: linear-gradient(180deg, #2a2a2a 0%, #232323 100%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .bg-white {
|
:root[data-theme="minecraft"] .bg-white {
|
||||||
background-color: #2d2d2d !important;
|
background-color: #292929 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .bg-zinc-50 {
|
:root[data-theme="minecraft"] .bg-zinc-50 {
|
||||||
background-color: #252525 !important;
|
background-color: #242424 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .bg-zinc-100 {
|
:root[data-theme="minecraft"] .bg-zinc-100 {
|
||||||
background-color: #343434 !important;
|
background-color: #323232 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .text-zinc-400,
|
:root[data-theme="minecraft"] .text-zinc-400,
|
||||||
:root[data-theme="minecraft"] .text-zinc-500,
|
:root[data-theme="minecraft"] .text-zinc-500,
|
||||||
:root[data-theme="minecraft"] .text-zinc-600,
|
:root[data-theme="minecraft"] .text-zinc-600,
|
||||||
:root[data-theme="minecraft"] .text-zinc-700 {
|
:root[data-theme="minecraft"] .text-zinc-700 {
|
||||||
color: #d0d0d0 !important;
|
color: #d7d7d7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .text-zinc-800,
|
||||||
|
:root[data-theme="minecraft"] .text-zinc-900 {
|
||||||
|
color: #ececec !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .text-blue-600,
|
:root[data-theme="minecraft"] .text-blue-600,
|
||||||
@@ -125,24 +154,26 @@
|
|||||||
|
|
||||||
:root[data-theme="minecraft"] .text-red-600,
|
:root[data-theme="minecraft"] .text-red-600,
|
||||||
:root[data-theme="minecraft"] .text-red-700 {
|
:root[data-theme="minecraft"] .text-red-700 {
|
||||||
color: var(--mc-redstone) !important;
|
color: var(--mc-red) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] button {
|
:root[data-theme="minecraft"] button {
|
||||||
background: linear-gradient(180deg, var(--mc-stone) 0%, var(--mc-stone-dark) 100%) !important;
|
background: linear-gradient(180deg, var(--mc-wood) 0%, var(--mc-wood-dark) 100%) !important;
|
||||||
border: 3px solid #000000 !important;
|
border: 3px solid #000 !important;
|
||||||
border-bottom-width: 7px !important;
|
border-bottom-width: 7px !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.45);
|
color: #fff !important;
|
||||||
color: #ffffff !important;
|
font-family: "PressStart2P", "VT323", sans-serif !important;
|
||||||
font-family: "DelaGothicOne", "MiSansBold", "MiSans", sans-serif;
|
font-size: 0.62rem !important;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.04em;
|
||||||
text-shadow: 1px 1px 0 #000000;
|
line-height: 1.4;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.48);
|
||||||
transition: transform 0.08s ease, filter 0.08s ease;
|
transition: transform 0.08s ease, filter 0.08s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] button:hover:not(:disabled) {
|
:root[data-theme="minecraft"] button:hover:not(:disabled) {
|
||||||
filter: brightness(1.08);
|
filter: brightness(1.07);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,23 +183,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] button:disabled {
|
:root[data-theme="minecraft"] button:disabled {
|
||||||
filter: saturate(0.25);
|
opacity: 0.68;
|
||||||
opacity: 0.7;
|
filter: saturate(0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] input,
|
:root[data-theme="minecraft"] input,
|
||||||
:root[data-theme="minecraft"] textarea,
|
:root[data-theme="minecraft"] textarea,
|
||||||
:root[data-theme="minecraft"] select {
|
:root[data-theme="minecraft"] select {
|
||||||
background: #1f1f1f !important;
|
background: #1f1f1f !important;
|
||||||
border: 3px solid #000000 !important;
|
border: 3px solid #000 !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.1);
|
color: #f4f4f4 !important;
|
||||||
color: #f2f2f2 !important;
|
box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.12);
|
||||||
|
font-family: "VT323", "MiSans", sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] input::placeholder,
|
:root[data-theme="minecraft"] input::placeholder,
|
||||||
:root[data-theme="minecraft"] textarea::placeholder {
|
:root[data-theme="minecraft"] textarea::placeholder {
|
||||||
color: #acacac;
|
color: #adadad;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] a {
|
:root[data-theme="minecraft"] a {
|
||||||
@@ -180,22 +213,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] table thead {
|
:root[data-theme="minecraft"] table thead {
|
||||||
background: #333333 !important;
|
background: #353535 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] table tr {
|
:root[data-theme="minecraft"] table tr {
|
||||||
border-color: #000000 !important;
|
border-color: #000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] pre {
|
:root[data-theme="minecraft"] pre {
|
||||||
border: 2px solid #000000;
|
border: 2px solid #000;
|
||||||
box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.08);
|
box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] code {
|
||||||
|
font-family: "VT323", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor,
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .margin,
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .monaco-editor-background,
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor-background,
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .inputarea.ime-input {
|
||||||
|
background-color: #111317 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .current-line {
|
||||||
|
border-color: #232b3c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .suggest-widget,
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .parameter-hints-widget {
|
||||||
|
border: 2px solid #000 !important;
|
||||||
|
background: #161a22 !important;
|
||||||
|
color: #ececec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .monaco-editor .suggest-widget .monaco-list-row.focused {
|
||||||
|
background: #2a3347 !important;
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] img {
|
:root[data-theme="minecraft"] img {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .pixel-avatar-frame {
|
||||||
|
border: 3px solid #000 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: linear-gradient(180deg, var(--mc-plank) 0%, var(--mc-dirt) 100%);
|
||||||
|
box-shadow:
|
||||||
|
3px 3px 0 rgba(0, 0, 0, 0.45),
|
||||||
|
inset 1px 1px 0 rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .pixel-avatar-image {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="minecraft"] .problem-markdown-compact {
|
:root[data-theme="minecraft"] .problem-markdown-compact {
|
||||||
font-size: 66%;
|
font-size: 66%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .problem-markdown-compact article {
|
||||||
|
color: #ececec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .problem-markdown-compact blockquote {
|
||||||
|
color: #d9d9d9 !important;
|
||||||
|
border-left-color: #8d6e63 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .problem-markdown-compact th,
|
||||||
|
:root[data-theme="minecraft"] .problem-markdown-compact td {
|
||||||
|
color: #ececec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="minecraft"] .problem-markdown-compact th {
|
||||||
|
background: #3a3a3a !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export type ThemeId = "default" | "minecraft";
|
|||||||
export type UiLanguage = "en" | "zh";
|
export type UiLanguage = "en" | "zh";
|
||||||
|
|
||||||
export const DEFAULT_THEME: ThemeId = "default";
|
export const DEFAULT_THEME: ThemeId = "default";
|
||||||
export const DEFAULT_LANGUAGE: UiLanguage = "en";
|
export const DEFAULT_LANGUAGE: UiLanguage = "zh";
|
||||||
|
|
||||||
export type ThemeMessages = Record<string, string>;
|
export type ThemeMessages = Record<string, string>;
|
||||||
|
|
||||||
|
|||||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 5.5 MiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 5.6 MiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 5.2 MiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 6.0 MiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 5.7 MiB |
@@ -0,0 +1,651 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1920, initial-scale=1.0">
|
||||||
|
<title>CSP Learning Platform - Minecraft Edition</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--grass-top: #7CB342;
|
||||||
|
--dirt-side: #795548;
|
||||||
|
--wood-frame: #5D4037;
|
||||||
|
--wood-bg: #A1887F;
|
||||||
|
--plank-light: #D7CCC8;
|
||||||
|
--gold-btn: #FFB300;
|
||||||
|
--gold-shadow: #BFA002;
|
||||||
|
--btn-shadow: #33691E;
|
||||||
|
--text-main: #3E2723;
|
||||||
|
--diamond-blue: #00B0D6;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 1920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
color: white;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #1f1f1f 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #1f1f1f 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #1f1f1f 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #1f1f1f 75%);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
background-position: 0 0, 0 20px, 20px -20px, -20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.pixel-text {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
height: 400px;
|
||||||
|
background-color: var(--grass-top);
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 20px solid var(--dirt-side);
|
||||||
|
box-shadow: inset 0 -10px 0 rgba(0,0,0,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minecraft Grass Pattern via CSS */
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,0.1) 2px, transparent 2px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.1) 2px, transparent 2px);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 4px 4px 0px rgba(0,0,0,0.5);
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #FFEB3B;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
font-size: 28px; /* VT323 is smaller visually */
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item i {
|
||||||
|
color: #FFEB3B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3D Pixel Button */
|
||||||
|
.btn-pixel {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 20px 40px;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
border: 4px solid rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green {
|
||||||
|
background-color: #558B2F;
|
||||||
|
box-shadow:
|
||||||
|
inset 4px 4px 0 rgba(255,255,255,0.3),
|
||||||
|
inset -4px -4px 0 rgba(0,0,0,0.3),
|
||||||
|
4px 4px 0 #1B5E20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green:active {
|
||||||
|
transform: translate(4px, 4px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold {
|
||||||
|
background-color: var(--gold-btn);
|
||||||
|
color: #3E2723;
|
||||||
|
box-shadow:
|
||||||
|
inset 4px 4px 0 rgba(255,255,255,0.4),
|
||||||
|
inset -4px -4px 0 rgba(0,0,0,0.2),
|
||||||
|
4px 4px 0 var(--gold-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold:active {
|
||||||
|
transform: translate(4px, 4px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Showcase Section */
|
||||||
|
.showcase {
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #C69C6D; /* Plank color */
|
||||||
|
border: 4px solid #3E2723;
|
||||||
|
padding: 8px;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.27);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
8px 8px 0 rgba(0,0,0,0.5),
|
||||||
|
inset 4px 4px 0 rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-10px) rotateX(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:last-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
width: 60%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
border: 2px solid #795548;
|
||||||
|
background: #EFEBE9;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 4px solid #3E2723;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
color: var(--text-main);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-features {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: #8D6E63;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
border: 2px solid #5D4037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
margin-top: auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view {
|
||||||
|
background: #795548;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #3E2723;
|
||||||
|
box-shadow: 2px 2px 0 #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view:hover {
|
||||||
|
background: #8D6E63;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Download Section */
|
||||||
|
.download-section {
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
border: 4px solid #795548;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
margin: 20px auto;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #ccc;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px dashed #795548;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list div {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Orbs Animation */
|
||||||
|
.orb {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #99ff33;
|
||||||
|
border: 2px solid #66cc00;
|
||||||
|
box-shadow: 0 0 5px #99ff33;
|
||||||
|
animation: float 4s infinite ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translateY(0px) rotate(0deg); opacity: 0; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background: #000;
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 4px solid #795548;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: #C69C6D;
|
||||||
|
border: 4px solid white;
|
||||||
|
padding: 40px;
|
||||||
|
width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
color: #3E2723;
|
||||||
|
box-shadow: 0 0 0 4px #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: #d32f2f;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #000;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Particle decoration on hero */
|
||||||
|
.hero-particle {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
animation: pixelFloat 10s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pixelFloat {
|
||||||
|
0% { transform: translateY(100vh); }
|
||||||
|
100% { transform: translateY(-100px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<header class="hero">
|
||||||
|
<!-- Floating particles -->
|
||||||
|
<div class="hero-particle" style="left: 10%; animation-delay: 0s; width: 20px; height: 20px;"></div>
|
||||||
|
<div class="hero-particle" style="left: 80%; animation-delay: 2s; width: 30px; height: 30px;"></div>
|
||||||
|
<div class="hero-particle" style="left: 40%; animation-delay: 5s; width: 15px; height: 15px;"></div>
|
||||||
|
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<i class="ri-box-3-fill" style="vertical-align: middle; color: #5D4037;"></i>
|
||||||
|
CSP Learning Platform
|
||||||
|
</h1>
|
||||||
|
<div class="hero-subtitle">Minecraft Pixel Art Style Edition</div>
|
||||||
|
<p class="hero-desc">Complete HTML package with 5 fully functional pages</p>
|
||||||
|
|
||||||
|
<div class="features-list">
|
||||||
|
<div class="feature-item"><i class="ri-check-line"></i> Pure HTML/CSS/JS</div>
|
||||||
|
<div class="feature-item"><i class="ri-wifi-off-line"></i> Offline Ready</div>
|
||||||
|
<div class="feature-item"><i class="ri-device-line"></i> Responsive Design</div>
|
||||||
|
<div class="feature-item"><i class="ri-grid-fill"></i> Pixel Perfect</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#showcase" class="btn-pixel btn-green" id="startBtn">Start Exploring</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container" id="showcase">
|
||||||
|
<div class="showcase">
|
||||||
|
|
||||||
|
<div class="grid-container">
|
||||||
|
<!-- Card 1: Login -->
|
||||||
|
<div class="card" onclick="window.location.href='#'">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-img-wrapper">
|
||||||
|
<img src="https://a.lovart.ai/artifacts/agent/4jJnMhBDygFtkHFr.png" alt="Login Page" class="card-img">
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">Login Page</h3>
|
||||||
|
<p class="card-desc">Minecraft-themed authentication with form validation.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="tag">Tab Switching</span>
|
||||||
|
<span class="tag">Password Strength</span>
|
||||||
|
<span class="tag">Mock Login</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-view">View Page <i class="ri-arrow-right-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 2: Problem Library -->
|
||||||
|
<div class="card" onclick="window.location.href='#'">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-img-wrapper">
|
||||||
|
<img src="https://a.lovart.ai/artifacts/agent/j6E654EFXn3NxJb4.png" alt="Problem Library" class="card-img">
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">Problem Library</h3>
|
||||||
|
<p class="card-desc">Browse 1,556 problems with advanced filtering and search.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="tag">Sidebar Menu</span>
|
||||||
|
<span class="tag">Search & Filter</span>
|
||||||
|
<span class="tag">Pagination</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-view">View Page <i class="ri-arrow-right-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 3: Code Editor -->
|
||||||
|
<div class="card" onclick="window.location.href='#'">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-img-wrapper">
|
||||||
|
<img src="https://a.lovart.ai/artifacts/agent/OWMy8aA6hD9v2JYA.png" alt="Code Editor" class="card-img">
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">Code Editor</h3>
|
||||||
|
<p class="card-desc">Split-panel editor with syntax highlighting and output.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="tag">Multi-language</span>
|
||||||
|
<span class="tag">Theme Toggle</span>
|
||||||
|
<span class="tag">Test Runner</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-view">View Page <i class="ri-arrow-right-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: Profile Center -->
|
||||||
|
<div class="card" onclick="window.location.href='#'">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-img-wrapper">
|
||||||
|
<img src="https://a.lovart.ai/artifacts/agent/zDDM5HzNib6if6te.png" alt="Profile Center" class="card-img">
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">Profile Center</h3>
|
||||||
|
<p class="card-desc">User dashboard with achievements, stats, and history.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="tag">XP System</span>
|
||||||
|
<span class="tag">Badges</span>
|
||||||
|
<span class="tag">Leaderboard</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button class="btn-view">View Page <i class="ri-arrow-right-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 5: Contest Page (Wide) -->
|
||||||
|
<div class="card" onclick="window.location.href='#'">
|
||||||
|
<div class="card-inner" style="flex-direction: row; gap: 20px;">
|
||||||
|
<div class="card-img-wrapper" style="width: 50%; height: 250px; margin-bottom: 0;">
|
||||||
|
<img src="https://a.lovart.ai/artifacts/agent/KTP6hqvX7SCjbsuf.png" alt="Contest Page" class="card-img">
|
||||||
|
</div>
|
||||||
|
<div class="card-content" style="width: 50%;">
|
||||||
|
<h3 class="card-title">Contest Page</h3>
|
||||||
|
<p class="card-desc">Live contests with real-time leaderboard and arena mode.</p>
|
||||||
|
<div class="card-features">
|
||||||
|
<span class="tag">Countdown Timer</span>
|
||||||
|
<span class="tag">Live Updates</span>
|
||||||
|
<span class="tag">Rankings</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions" style="margin-top: 20px;">
|
||||||
|
<button class="btn-view">View Page <i class="ri-arrow-right-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download Section -->
|
||||||
|
<div class="download-section">
|
||||||
|
<h2 class="pixel-text" style="color: white; margin-bottom: 20px; font-size: 28px;">Ready to Craft Your Code?</h2>
|
||||||
|
<div class="file-list">
|
||||||
|
<div><i class="ri-file-zip-line"></i> csp-platform-v1.0.zip (12.5 MB)</div>
|
||||||
|
<div><i class="ri-file-list-line"></i> README.txt (2 KB)</div>
|
||||||
|
<div><i class="ri-folder-line"></i> /assets (images, css, js)</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<button class="btn-pixel btn-gold" id="downloadBtn">
|
||||||
|
<i class="ri-download-cloud-2-line"></i> Download Package
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 20px; color: #aaa; font-size: 18px;">Installation: Simply unzip and open index.html in any browser.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<p>Crafted with <i class="ri-heart-fill" style="color: #d32f2f;"></i> by HTML Generator | Version 1.19.2</p>
|
||||||
|
<p style="margin-top: 10px; font-size: 14px; opacity: 0.6;">Mock GitHub Link: github.com/user/csp-minecraft</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Download Modal -->
|
||||||
|
<div class="modal" id="downloadModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="modal-close" onclick="closeModal()">X</button>
|
||||||
|
<h3 class="modal-title">Generating World...</h3>
|
||||||
|
<p style="font-size: 20px; margin-bottom: 20px;">Your download is being prepared!</p>
|
||||||
|
<div style="background: #3E2723; padding: 10px; margin-top: 20px; position: relative; height: 30px;">
|
||||||
|
<div style="background: #76FF03; width: 0%; height: 100%; transition: width 2s;" id="progressBar"></div>
|
||||||
|
</div>
|
||||||
|
<p id="progressText" style="margin-top: 10px;">0%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Smooth scroll
|
||||||
|
document.getElementById('startBtn').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download Modal Logic
|
||||||
|
const modal = document.getElementById('downloadModal');
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
const progressBar = document.getElementById('progressBar');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
|
||||||
|
downloadBtn.addEventListener('click', () => {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressText.innerText = '0%';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
let progress = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
progress += 5;
|
||||||
|
progressText.innerText = progress + '%';
|
||||||
|
if (progress >= 100) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setTimeout(() => {
|
||||||
|
progressText.innerText = 'Download Complete!';
|
||||||
|
// Normally trigger download here
|
||||||
|
setTimeout(closeModal, 1500);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// XP Orbs Generation
|
||||||
|
function createOrb() {
|
||||||
|
const orb = document.createElement('div');
|
||||||
|
orb.className = 'orb';
|
||||||
|
|
||||||
|
// Random position
|
||||||
|
const x = Math.random() * window.innerWidth;
|
||||||
|
orb.style.left = x + 'px';
|
||||||
|
orb.style.bottom = '-20px'; // Start from bottom
|
||||||
|
|
||||||
|
// Random size variation
|
||||||
|
const size = 5 + Math.random() * 10;
|
||||||
|
orb.style.width = size + 'px';
|
||||||
|
orb.style.height = size + 'px';
|
||||||
|
|
||||||
|
document.body.appendChild(orb);
|
||||||
|
|
||||||
|
// Remove after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
orb.remove();
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(createOrb, 800);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Minecraft Code Editor</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'DelaGothicOne';
|
||||||
|
src: url('https://assets-persist.lovart.ai/agent-static-assets/DelaGothicOne-Regular.ttf');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MiSans';
|
||||||
|
src: url('https://assets-persist.lovart.ai/agent-static-assets/MiSans-Regular.ttf');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'PixelCode';
|
||||||
|
src: local('Courier New'), monospace; /* Fallback for code */
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mc-wood-light: #A07F53;
|
||||||
|
--mc-wood-dark: #765637;
|
||||||
|
--mc-stone: #757575;
|
||||||
|
--mc-stone-dark: #3b3b3b;
|
||||||
|
--mc-obsidian: #18181F;
|
||||||
|
--mc-grass: #7CB342;
|
||||||
|
--mc-grass-dark: #558B2F;
|
||||||
|
--mc-gold: #FFB300;
|
||||||
|
--mc-gold-dark: #FF8F00;
|
||||||
|
--mc-diamond: #00BCD4;
|
||||||
|
--mc-redstone: #F44336;
|
||||||
|
--mc-text-shadow: 2px 2px 0px #000;
|
||||||
|
--mc-border-light: rgba(255, 255, 255, 0.4);
|
||||||
|
--mc-border-dark: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px; /* Fixed height for preview, though body usually auto */
|
||||||
|
background-color: #121212;
|
||||||
|
font-family: 'MiSans', sans-serif;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minecraft Texture Classes */
|
||||||
|
.bg-wood {
|
||||||
|
background-color: var(--mc-wood-dark);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgba(0,0,0,0.1) 50%, transparent 50%),
|
||||||
|
linear-gradient(rgba(0,0,0,0.1) 50%, transparent 50%);
|
||||||
|
background-size: 4px 4px;
|
||||||
|
box-shadow: inset 0 0 40px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-stone {
|
||||||
|
background-color: var(--mc-stone);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 2px 2px, rgba(255,255,255,0.1) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 10px 10px, rgba(0,0,0,0.1) 2px, transparent 2px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-obsidian {
|
||||||
|
background-color: #1a1a24;
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 10px),
|
||||||
|
repeating-linear-gradient(-45deg, rgba(255,255,255,0.02) 0px, rgba(255,255,255,0.02) 1px, transparent 1px, transparent 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1, h2, h3, .mc-font {
|
||||||
|
font-family: 'DelaGothicOne', cursive;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-shadow: 2px 2px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3D Button Style */
|
||||||
|
.mc-btn {
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-top-color: rgba(255,255,255,0.5);
|
||||||
|
border-left-color: rgba(255,255,255,0.5);
|
||||||
|
border-bottom-color: rgba(0,0,0,0.5);
|
||||||
|
border-right-color: rgba(0,0,0,0.5);
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-family: 'DelaGothicOne', cursive;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-btn:active {
|
||||||
|
border-top-color: rgba(0,0,0,0.5);
|
||||||
|
border-left-color: rgba(0,0,0,0.5);
|
||||||
|
border-bottom-color: rgba(255,255,255,0.5);
|
||||||
|
border-right-color: rgba(255,255,255,0.5);
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green { background-color: var(--mc-grass); color: white; text-shadow: 1px 1px 0 #000; }
|
||||||
|
.btn-gold { background-color: var(--mc-gold); color: #3e2723; text-shadow: none; }
|
||||||
|
.btn-stone { background-color: var(--mc-stone); color: white; }
|
||||||
|
.btn-icon { padding: 8px; }
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 12px; height: 12px; }
|
||||||
|
::-webkit-scrollbar-track { background: #2b2b2b; }
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #555;
|
||||||
|
border: 2px solid #2b2b2b;
|
||||||
|
border-top-color: #777;
|
||||||
|
border-left-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.navbar {
|
||||||
|
height: 60px;
|
||||||
|
background-color: #212121;
|
||||||
|
border-bottom: 4px solid #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
gap: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.nav-logo { font-size: 24px; color: var(--mc-grass); }
|
||||||
|
.nav-item { color: #aaa; text-decoration: none; font-family: 'DelaGothicOne'; font-size: 14px; }
|
||||||
|
.nav-item:hover { color: white; text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Main Layout */
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Panel */
|
||||||
|
.left-panel {
|
||||||
|
width: 40%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 4px solid #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-card {
|
||||||
|
background-color: #FDF5E6; /* Old Lace */
|
||||||
|
color: #333;
|
||||||
|
padding: 24px;
|
||||||
|
border: 4px solid #333;
|
||||||
|
box-shadow: 8px 8px 0 rgba(0,0,0,0.3);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -4px; left: -4px; right: -4px; bottom: -4px;
|
||||||
|
border: 2px solid #5d4037;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-badge {
|
||||||
|
background-color: var(--mc-grass);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 2px solid #33691E;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
font-family: 'DelaGothicOne';
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block-display {
|
||||||
|
background-color: #263238;
|
||||||
|
color: #eceff1;
|
||||||
|
padding: 12px;
|
||||||
|
border-left: 4px solid var(--mc-grass);
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
margin: 10px 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
height: 70px;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-top: 4px solid #3e2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Panel */
|
||||||
|
.right-panel {
|
||||||
|
width: 60%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
height: 56px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 4px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-editor-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
width: 48px;
|
||||||
|
background-color: #121217;
|
||||||
|
color: #546e7a;
|
||||||
|
text-align: right;
|
||||||
|
padding: 16px 8px;
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
user-select: none;
|
||||||
|
border-right: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-area {
|
||||||
|
flex: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Syntax Highlighting Colors */
|
||||||
|
.token-keyword { color: #cc7832; font-weight: bold; }
|
||||||
|
.token-type { color: #e0c46c; }
|
||||||
|
.token-string { color: #6a8759; }
|
||||||
|
.token-comment { color: #808080; font-style: italic; }
|
||||||
|
.token-number { color: #6897bb; }
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
height: 32px;
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-top: 2px solid #444;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
gap: 20px;
|
||||||
|
color: #aaa;
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-counter {
|
||||||
|
color: var(--mc-gold);
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test Panel */
|
||||||
|
.test-panel {
|
||||||
|
height: 220px;
|
||||||
|
border-top: 4px solid #000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: height 0.3s;
|
||||||
|
}
|
||||||
|
.test-panel.collapsed { height: 40px; }
|
||||||
|
|
||||||
|
.test-header {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
border-bottom: 2px solid #444;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'DelaGothicOne';
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.tab-btn.active { color: white; border-bottom: 2px solid var(--mc-grass); }
|
||||||
|
|
||||||
|
.test-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-case-item {
|
||||||
|
flex: 1;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
border: 2px solid #444;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.test-case-status { display: flex; justify-content: space-between; margin-bottom: 8px; font-weight: bold; }
|
||||||
|
.status-pass { color: var(--mc-grass); }
|
||||||
|
.status-fail { color: var(--mc-redstone); }
|
||||||
|
|
||||||
|
/* XP Overlay */
|
||||||
|
.xp-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 64px;
|
||||||
|
color: #B9F6CA;
|
||||||
|
text-shadow: 4px 4px 0 #1B5E20;
|
||||||
|
font-family: 'DelaGothicOne';
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes xpFloat {
|
||||||
|
0% { opacity: 0; transform: translate(-50%, -20%); }
|
||||||
|
20% { opacity: 1; transform: translate(-50%, -50%); }
|
||||||
|
80% { opacity: 1; transform: translate(-50%, -60%); }
|
||||||
|
100% { opacity: 0; transform: translate(-50%, -100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-xp { animation: xpFloat 2s ease-out forwards; }
|
||||||
|
|
||||||
|
/* Confetti */
|
||||||
|
.confetti {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px; height: 8px;
|
||||||
|
background: var(--mc-diamond);
|
||||||
|
animation: fall 3s linear forwards;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
@keyframes fall {
|
||||||
|
to { transform: translateY(100vh) rotate(720deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select styling */
|
||||||
|
.mc-select {
|
||||||
|
background: var(--mc-stone-dark);
|
||||||
|
border: 2px solid #000;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-family: 'PixelCode', monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Top Navigation -->
|
||||||
|
<nav class="navbar bg-stone">
|
||||||
|
<div class="nav-logo mc-font"><i class="ri-box-3-fill"></i> MINECODE</div>
|
||||||
|
<a href="#" class="nav-item">Problems</a>
|
||||||
|
<a href="#" class="nav-item">Contest</a>
|
||||||
|
<a href="#" class="nav-item">Discuss</a>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<div class="nav-item" style="color:var(--mc-gold)">LVL 24</div>
|
||||||
|
<img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=Steve" alt="User" style="width: 32px; height: 32px; border: 2px solid white;">
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
|
||||||
|
<!-- Left Panel: Problem -->
|
||||||
|
<aside class="left-panel bg-wood">
|
||||||
|
<div class="problem-content">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: 20px;">
|
||||||
|
<h1 style="font-size: 28px; color: #3E2723;">1. Two Sum</h1>
|
||||||
|
<span class="difficulty-badge">EASY</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="paper-card">
|
||||||
|
<p style="margin-bottom: 16px; line-height: 1.6;">
|
||||||
|
Given an array of integers <code>nums</code> and an integer <code>target</code>, return <i>indices</i> of the two numbers such that they add up to <code>target</code>.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
You may assume that each input would have <strong>exactly one solution</strong>, and you may not use the same element twice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color:#3E2723; margin-bottom:10px;">Example 1:</h3>
|
||||||
|
<div class="code-block-display">Input: nums = [2,7,11,15], target = 9
|
||||||
|
Output: [0,1]
|
||||||
|
Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color:#3E2723; margin-bottom:10px;">Example 2:</h3>
|
||||||
|
<div class="code-block-display">Input: nums = [3,2,4], target = 6
|
||||||
|
Output: [1,2]</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color:#3E2723; margin-bottom:10px; display:flex; align-items:center;">
|
||||||
|
<i class="ri-book-mark-fill" style="margin-right:8px;"></i> Hints
|
||||||
|
</h3>
|
||||||
|
<div style="background: rgba(0,0,0,0.1); padding: 10px; border: 2px dashed #5D4037; color: #3E2723;">
|
||||||
|
Try using a Hash Map to store visited numbers.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-footer">
|
||||||
|
<button class="mc-btn btn-stone"><i class="ri-star-fill"></i> Bookmark</button>
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<button class="mc-btn btn-stone btn-icon"><i class="ri-share-forward-fill"></i></button>
|
||||||
|
<button class="mc-btn btn-stone btn-icon"><i class="ri-file-list-3-fill"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Right Panel: Editor -->
|
||||||
|
<main class="right-panel">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="toolbar bg-stone">
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<select class="mc-select">
|
||||||
|
<option>C++</option>
|
||||||
|
<option>Java</option>
|
||||||
|
<option>Python</option>
|
||||||
|
</select>
|
||||||
|
<button class="mc-btn btn-stone btn-icon" style="padding:4px 8px;"><i class="ri-settings-3-fill"></i></button>
|
||||||
|
<div style="color:#bbb; font-size:12px;">Size: 14px</div>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button class="mc-btn btn-green" onclick="runCode()"><i class="ri-play-fill"></i> Run</button>
|
||||||
|
<button class="mc-btn btn-gold" onclick="submitCode()"><i class="ri-upload-cloud-2-fill"></i> Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code Editor -->
|
||||||
|
<div class="code-editor-wrapper bg-obsidian">
|
||||||
|
<div class="line-numbers">
|
||||||
|
1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>11
|
||||||
|
</div>
|
||||||
|
<div class="code-area" contenteditable="true" spellcheck="false">
|
||||||
|
<span class="token-keyword">class</span> Solution {
|
||||||
|
<span class="token-keyword">public</span>:
|
||||||
|
<span class="token-type">vector</span><<span class="token-type">int</span>> twoSum(<span class="token-type">vector</span><<span class="token-type">int</span>>& nums, <span class="token-type">int</span> target) {
|
||||||
|
<span class="token-type">unordered_map</span><<span class="token-type">int</span>, <span class="token-type">int</span>> hash;
|
||||||
|
|
||||||
|
<span class="token-keyword">for</span> (<span class="token-type">int</span> i = <span class="token-number">0</span>; i < nums.size(); i++) {
|
||||||
|
<span class="token-type">int</span> complement = target - nums[i];
|
||||||
|
|
||||||
|
<span class="token-keyword">if</span> (hash.find(complement) != hash.end()) {
|
||||||
|
<span class="token-keyword">return</span> {hash[complement], i};
|
||||||
|
}
|
||||||
|
hash[nums[i]] = i;
|
||||||
|
}
|
||||||
|
<span class="token-keyword">return</span> {};
|
||||||
|
}
|
||||||
|
};</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<span>Ready</span>
|
||||||
|
<span style="border-left: 1px solid #555; height: 16px;"></span>
|
||||||
|
<span><i class="ri-time-fill"></i> 0ms</span>
|
||||||
|
<span><i class="ri-sd-card-mini-fill"></i> 0KB</span>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<div class="combo-counter"><i class="ri-fire-fill"></i> COMBO x5</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Panel -->
|
||||||
|
<div class="test-panel bg-stone" id="testPanel">
|
||||||
|
<div class="test-header" onclick="toggleTestPanel()">
|
||||||
|
<i class="ri-terminal-box-fill" style="margin-right: 10px; color: #ccc;"></i>
|
||||||
|
<span class="mc-font" style="font-size: 14px; color: #eee; flex:1">Test Console</span>
|
||||||
|
<i class="ri-arrow-up-s-line" id="toggleIcon"></i>
|
||||||
|
</div>
|
||||||
|
<div style="background: rgba(0,0,0,0.4); padding: 0 16px; display: flex; border-bottom: 2px solid #444;">
|
||||||
|
<button class="tab-btn active">Test Cases</button>
|
||||||
|
<button class="tab-btn">Result</button>
|
||||||
|
</div>
|
||||||
|
<div class="test-content">
|
||||||
|
<div class="test-case-item">
|
||||||
|
<div class="test-case-status">Case 1 <i class="ri-checkbox-circle-fill status-pass"></i></div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Input</div>
|
||||||
|
<div style="color: #fff; margin-bottom: 8px;">nums = [2,7,11,15], target = 9</div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Expected</div>
|
||||||
|
<div style="color: #fff;">[0,1]</div>
|
||||||
|
</div>
|
||||||
|
<div class="test-case-item">
|
||||||
|
<div class="test-case-status">Case 2 <i class="ri-checkbox-circle-fill status-pass"></i></div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Input</div>
|
||||||
|
<div style="color: #fff; margin-bottom: 8px;">nums = [3,2,4], target = 6</div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Expected</div>
|
||||||
|
<div style="color: #fff;">[1,2]</div>
|
||||||
|
</div>
|
||||||
|
<div class="test-case-item" style="border-color: #F44336; opacity: 0.8;">
|
||||||
|
<div class="test-case-status">Case 3 <i class="ri-close-circle-fill status-fail"></i></div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Input</div>
|
||||||
|
<div style="color: #fff; margin-bottom: 8px;">nums = [3,3], target = 6</div>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">Expected</div>
|
||||||
|
<div style="color: #fff;">[0,1]</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- XP Animation Overlay -->
|
||||||
|
<div id="xpDisplay" class="xp-overlay">+50 XP</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTestPanel() {
|
||||||
|
const panel = document.getElementById('testPanel');
|
||||||
|
const icon = document.getElementById('toggleIcon');
|
||||||
|
panel.classList.toggle('collapsed');
|
||||||
|
|
||||||
|
if(panel.classList.contains('collapsed')) {
|
||||||
|
icon.classList.remove('ri-arrow-down-s-line');
|
||||||
|
icon.classList.add('ri-arrow-up-s-line');
|
||||||
|
} else {
|
||||||
|
icon.classList.remove('ri-arrow-up-s-line');
|
||||||
|
icon.classList.add('ri-arrow-down-s-line');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCode() {
|
||||||
|
// Simulate running code
|
||||||
|
const btn = document.querySelector('.btn-green');
|
||||||
|
btn.innerHTML = '<i class="ri-loader-4-line ri-spin"></i> Running...';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = '<i class="ri-play-fill"></i> Run';
|
||||||
|
document.getElementById('testPanel').classList.remove('collapsed');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCode() {
|
||||||
|
const btn = document.querySelector('.btn-gold');
|
||||||
|
const originalContent = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="ri-loader-4-line ri-spin"></i> ...';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = originalContent;
|
||||||
|
showXP();
|
||||||
|
createConfetti();
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showXP() {
|
||||||
|
const xp = document.getElementById('xpDisplay');
|
||||||
|
xp.classList.remove('animate-xp');
|
||||||
|
void xp.offsetWidth; // trigger reflow
|
||||||
|
xp.classList.add('animate-xp');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfetti() {
|
||||||
|
const colors = ['#F44336', '#2196F3', '#FFEB3B', '#4CAF50', '#FF9800'];
|
||||||
|
for(let i=0; i<50; i++) {
|
||||||
|
const conf = document.createElement('div');
|
||||||
|
conf.className = 'confetti';
|
||||||
|
conf.style.left = Math.random() * 100 + 'vw';
|
||||||
|
conf.style.top = '-10px';
|
||||||
|
conf.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
conf.style.animationDuration = (Math.random() * 2 + 2) + 's';
|
||||||
|
document.body.appendChild(conf);
|
||||||
|
|
||||||
|
setTimeout(() => conf.remove(), 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,905 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1920, initial-scale=1.0">
|
||||||
|
<title>Minecraft Contest Page</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mc-bg-dark: #1D1D1D;
|
||||||
|
--mc-obsidian: #141019;
|
||||||
|
--mc-stone: #757575;
|
||||||
|
--mc-wood: #8B6914; /* Oak wood plank approximation */
|
||||||
|
--mc-wood-dark: #5C4033;
|
||||||
|
--mc-grass: #7CB342;
|
||||||
|
--mc-grass-dark: #558B2F;
|
||||||
|
--mc-gold: #FFB300;
|
||||||
|
--mc-redstone: #E53935;
|
||||||
|
--mc-diamond: #40C4FF;
|
||||||
|
--mc-white: #FFFFFF;
|
||||||
|
--mc-text-shadow: 2px 2px 0px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
background-color: #121212;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #1a1a1a 25%, transparent 25%, transparent 75%, #1a1a1a 75%, #1a1a1a),
|
||||||
|
linear-gradient(45deg, #1a1a1a 25%, transparent 25%, transparent 75%, #1a1a1a 75%, #1a1a1a);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
background-position: 0 0, 20px 20px;
|
||||||
|
color: var(--mc-white);
|
||||||
|
width: 1920px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Utilities --- */
|
||||||
|
.pixel-border {
|
||||||
|
box-shadow:
|
||||||
|
-4px 0 0 0 black,
|
||||||
|
4px 0 0 0 black,
|
||||||
|
0 -4px 0 0 black,
|
||||||
|
0 4px 0 0 black,
|
||||||
|
inset -4px -4px 0 0 rgba(0,0,0,0.5),
|
||||||
|
inset 4px 4px 0 0 rgba(255,255,255,0.2);
|
||||||
|
border: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow: var(--mc-text-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Navigation --- */
|
||||||
|
.navbar {
|
||||||
|
height: 80px;
|
||||||
|
background-color: #333;
|
||||||
|
border-bottom: 4px solid #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 40px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--mc-grass);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
color: #ccc;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 30px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.nav-links a.active {
|
||||||
|
color: var(--mc-gold);
|
||||||
|
text-shadow: 2px 2px 0 #5C4033;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Banner --- */
|
||||||
|
.banner {
|
||||||
|
height: 350px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(180deg, #2a0e36 0%, #46142e 100%); /* Nether-ish */
|
||||||
|
background-image: url('https://images.unsplash.com/photo-1628151015968-3a4429e9ef04?q=80&w=2072&auto=format&fit=crop'); /* Pixel art background placeholder */
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 6px solid #000;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-badge {
|
||||||
|
background-color: var(--mc-redstone);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(229, 57, 53, 0.7); }
|
||||||
|
70% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(229, 57, 53, 0); }
|
||||||
|
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(229, 57, 53, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-title {
|
||||||
|
font-size: 42px;
|
||||||
|
color: var(--mc-gold);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
-webkit-text-stroke: 2px #5C4033;
|
||||||
|
text-shadow: 4px 4px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
text-shadow: 3px 3px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-btn {
|
||||||
|
background-color: var(--mc-grass);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 20px 40px;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
transition: transform 0.1s, background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 6px 0 #558B2F, 0 10px 10px rgba(0,0,0,0.3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-btn:hover {
|
||||||
|
background-color: #8BC34A;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 0 #558B2F, 0 12px 12px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.join-btn:active {
|
||||||
|
transform: translateY(4px);
|
||||||
|
box-shadow: 0 2px 0 #558B2F, 0 4px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants-count {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.player-head {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #555;
|
||||||
|
display: inline-block;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Main Layout --- */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
gap: 40px;
|
||||||
|
max-width: 1800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-col {
|
||||||
|
flex: 0 0 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col {
|
||||||
|
flex: 0 0 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Tab Nav --- */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
background-color: #555;
|
||||||
|
padding: 15px 30px;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: none;
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
background-color: var(--mc-wood);
|
||||||
|
color: #fff;
|
||||||
|
top: 0;
|
||||||
|
padding-bottom: 19px;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
box-shadow:
|
||||||
|
-4px 0 0 0 black,
|
||||||
|
4px 0 0 0 black,
|
||||||
|
0 -4px 0 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Wood Panel Style --- */
|
||||||
|
.wood-panel {
|
||||||
|
background-color: var(--mc-wood);
|
||||||
|
border: 4px solid #000;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: inset 0 0 0 4px rgba(255,255,255,0.1), 8px 8px 0 rgba(0,0,0,0.5);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wood-texture {
|
||||||
|
/* Simulating wood grain with linear gradients */
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgba(0,0,0,0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(rgba(0,0,0,0.05) 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Contest Cards --- */
|
||||||
|
.contest-card {
|
||||||
|
background-color: #6D4C41; /* Darker wood */
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
position: relative;
|
||||||
|
border: 4px solid #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-card:hover {
|
||||||
|
transform: translateX(10px);
|
||||||
|
background-color: #795548;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #3E2723;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
color: var(--mc-gold);
|
||||||
|
border: 4px solid #000;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info h3 {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-details {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #D7CCC8;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars { color: var(--mc-gold); }
|
||||||
|
|
||||||
|
.card-action .btn-small {
|
||||||
|
background-color: var(--mc-stone);
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 10px 20px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
box-shadow: 0 4px 0 #424242;
|
||||||
|
}
|
||||||
|
.card-action .btn-small:hover {
|
||||||
|
background-color: #9E9E9E;
|
||||||
|
margin-top: -2px;
|
||||||
|
box-shadow: 0 6px 0 #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Leaderboard --- */
|
||||||
|
.leaderboard-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.live-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: var(--mc-grass);
|
||||||
|
border-radius: 50%; /* Minecraft has no circles, but for indicator */
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
box-shadow: 0 0 10px var(--mc-grass);
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink { 50% { opacity: 0.5; } }
|
||||||
|
|
||||||
|
.leaderboard-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 15px;
|
||||||
|
color: #3E2723;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 4px solid #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table td {
|
||||||
|
background-color: #5D4037;
|
||||||
|
padding: 15px;
|
||||||
|
color: #fff;
|
||||||
|
border-top: 4px solid #3E2723;
|
||||||
|
border-bottom: 4px solid #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-table tr td:first-child { border-left: 4px solid #3E2723; }
|
||||||
|
.leaderboard-table tr td:last-child { border-right: 4px solid #3E2723; }
|
||||||
|
|
||||||
|
.rank-1 td { background-color: #FFECB3; color: #5D4037; border-color: #FFB300 !important; }
|
||||||
|
.rank-2 td { background-color: #F5F5F5; color: #5D4037; border-color: #BDBDBD !important; }
|
||||||
|
.rank-3 td { background-color: #D7CCC8; color: #5D4037; border-color: #8D6E63 !important; }
|
||||||
|
|
||||||
|
.current-user td {
|
||||||
|
background-color: #DCEDC8;
|
||||||
|
color: #33691E;
|
||||||
|
border-color: #7CB342 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-icon { margin-right: 5px; }
|
||||||
|
|
||||||
|
/* --- Right Column --- */
|
||||||
|
.info-card {
|
||||||
|
background-color: #424242;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: 4px solid #000;
|
||||||
|
box-shadow: 8px 8px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-header {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--mc-gold);
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
border-bottom: 4px solid #000;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.stat-val { color: var(--mc-diamond); }
|
||||||
|
|
||||||
|
.medals-display {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
background: #212121;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 7px;
|
||||||
|
top: 20px;
|
||||||
|
bottom: -20px;
|
||||||
|
width: 2px;
|
||||||
|
background: #666;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.timeline-item:last-child .timeline-line { display: none; }
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--mc-gold);
|
||||||
|
border: 2px solid #000;
|
||||||
|
z-index: 1;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.timeline-date { color: #888; font-size: 10px; margin-bottom: 4px; }
|
||||||
|
.timeline-rank { color: #fff; }
|
||||||
|
|
||||||
|
.collapsible-content {
|
||||||
|
max-height: 500px; /* arbitrary large */
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.collapsed .collapsible-content {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Animations --- */
|
||||||
|
@keyframes shine {
|
||||||
|
0% { background-position: -100px; }
|
||||||
|
40%, 100% { background-position: 140px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-1 {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light ray effect using psuedo elements not easy on tr, applied to td */
|
||||||
|
.rank-1 td {
|
||||||
|
background: linear-gradient(120deg, #FFECB3 0%, #FFECB3 40%, #FFF9C4 50%, #FFECB3 60%, #FFECB3 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shine-gold 3s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shine-gold {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container { flex-direction: column; }
|
||||||
|
.left-col, .right-col { flex: 1 1 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar pixel-border">
|
||||||
|
<div class="logo text-shadow"><i class="ri-sword-fill"></i> ALGO CRAFT</div>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="#" class="active">CONTESTS</a>
|
||||||
|
<a href="#">PROBLEMS</a>
|
||||||
|
<a href="#">DISCUSS</a>
|
||||||
|
<a href="#">STORE</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:10px;">
|
||||||
|
<div style="width:32px; height:32px; background:#ddd; border:2px solid #000;">
|
||||||
|
<img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=Felix" alt="User" style="width:100%; height:100%;">
|
||||||
|
</div>
|
||||||
|
<span>STEVE_DEV</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="banner">
|
||||||
|
<div class="banner-overlay"></div>
|
||||||
|
<div class="banner-content">
|
||||||
|
<div class="live-badge pixel-border">LIVE NOW</div>
|
||||||
|
<h1 class="contest-title">WEEKLY ALGO CHALLENGE #42</h1>
|
||||||
|
<div class="countdown" id="countdown">02:45:30</div>
|
||||||
|
|
||||||
|
<button class="join-btn pixel-border">
|
||||||
|
JOIN CONTEST
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="participants-count">
|
||||||
|
<div class="player-head"></div>
|
||||||
|
<div class="player-head"></div>
|
||||||
|
<div class="player-head"></div>
|
||||||
|
<span>1,234 Crafters Online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div class="left-col">
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab active pixel-border" onclick="switchTab('ongoing')">ONGOING</div>
|
||||||
|
<div class="tab pixel-border" onclick="switchTab('upcoming')">UPCOMING</div>
|
||||||
|
<div class="tab pixel-border" onclick="switchTab('finished')">FINISHED</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wood-panel pixel-border wood-texture" id="contest-list">
|
||||||
|
<!-- Contest Cards -->
|
||||||
|
<div class="contest-card pixel-border">
|
||||||
|
<div class="flex-center">
|
||||||
|
<div class="card-icon pixel-border"><i class="ri-trophy-fill"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<h3>Weekly Challenge #42</h3>
|
||||||
|
<div class="card-details">
|
||||||
|
<span><i class="ri-time-line"></i> Ends in 2h</span>
|
||||||
|
<span><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i></span>
|
||||||
|
<span style="color:#4DB6AC">500 XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="btn-small">ENTER</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contest-card pixel-border">
|
||||||
|
<div class="flex-center">
|
||||||
|
<div class="card-icon pixel-border"><i class="ri-sword-fill" style="color:#CFD8DC"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<h3>Bi-Weekly Rumble #15</h3>
|
||||||
|
<div class="card-details">
|
||||||
|
<span><i class="ri-calendar-line"></i> Sat, 14:00</span>
|
||||||
|
<span><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i><i class="ri-star-line"></i></span>
|
||||||
|
<span style="color:#4DB6AC">350 XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="btn-small">REGISTER</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contest-card pixel-border">
|
||||||
|
<div class="flex-center">
|
||||||
|
<div class="card-icon pixel-border"><i class="ri-vip-diamond-fill" style="color:var(--mc-diamond)"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<h3>Diamond League Qualifiers</h3>
|
||||||
|
<div class="card-details">
|
||||||
|
<span><i class="ri-calendar-line"></i> Sun, 10:00</span>
|
||||||
|
<span><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i><i class="ri-star-fill stars"></i></span>
|
||||||
|
<span style="color:#4DB6AC">1000 XP + BADGE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="btn-small">REGISTER</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Leaderboard -->
|
||||||
|
<div class="wood-panel pixel-border wood-texture">
|
||||||
|
<div class="leaderboard-title">
|
||||||
|
<h2 class="text-shadow" style="color:#3E2723">LEADERBOARD</h2>
|
||||||
|
<div style="font-size: 12px; color: #3E2723; display: flex; align-items: center;">
|
||||||
|
<span class="live-dot"></span> UPDATING LIVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="leaderboard-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>PLAYER</th>
|
||||||
|
<th>SCORE</th>
|
||||||
|
<th>TIME</th>
|
||||||
|
<th>STATUS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="leaderboard-body">
|
||||||
|
<!-- JS will populate -->
|
||||||
|
<tr class="rank-1 pixel-border">
|
||||||
|
<td><i class="ri-vip-crown-fill" style="color:#F57F17"></i> 1</td>
|
||||||
|
<td>Notch_Real</td>
|
||||||
|
<td>400</td>
|
||||||
|
<td>00:45:12</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rank-2">
|
||||||
|
<td><i class="ri-medal-fill" style="color:#757575"></i> 2</td>
|
||||||
|
<td>Alex_Pro</td>
|
||||||
|
<td>380</td>
|
||||||
|
<td>00:52:30</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="rank-3">
|
||||||
|
<td><i class="ri-medal-fill" style="color:#8D6E63"></i> 3</td>
|
||||||
|
<td>CreeperAwMan</td>
|
||||||
|
<td>350</td>
|
||||||
|
<td>01:05:00</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>4</td>
|
||||||
|
<td>Enderman_tp</td>
|
||||||
|
<td>320</td>
|
||||||
|
<td>01:10:22</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>5</td>
|
||||||
|
<td>RedstoneEng</td>
|
||||||
|
<td>300</td>
|
||||||
|
<td>01:15:45</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>6</td>
|
||||||
|
<td>Miner64</td>
|
||||||
|
<td>280</td>
|
||||||
|
<td>01:20:10</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="current-user">
|
||||||
|
<td>7</td>
|
||||||
|
<td>STEVE_DEV (YOU)</td>
|
||||||
|
<td>250</td>
|
||||||
|
<td>01:30:00</td>
|
||||||
|
<td><span style="color:orange">WA (1)</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>8</td>
|
||||||
|
<td>ZombieBoi</td>
|
||||||
|
<td>200</td>
|
||||||
|
<td>01:35:12</td>
|
||||||
|
<td><span style="color:green">AC</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="right-col">
|
||||||
|
|
||||||
|
<!-- Stats Card -->
|
||||||
|
<div class="info-card pixel-border">
|
||||||
|
<div class="info-header">
|
||||||
|
<span>MY STATS</span>
|
||||||
|
<i class="ri-bar-chart-fill"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Participated:</span>
|
||||||
|
<span class="stat-val">24</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Best Rank:</span>
|
||||||
|
<span class="stat-val">#3 🥉</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Total Points:</span>
|
||||||
|
<span class="stat-val">1,250</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>Rating:</span>
|
||||||
|
<span class="stat-val" style="color:var(--mc-gold)">1650 (Diamond II)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="medals-display pixel-border">
|
||||||
|
<div style="text-align:center">
|
||||||
|
<i class="ri-medal-fill" style="color:var(--mc-gold); font-size:20px"></i>
|
||||||
|
<div style="font-size:10px">2</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<i class="ri-medal-fill" style="color:#BDBDBD; font-size:20px"></i>
|
||||||
|
<div style="font-size:10px">5</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<i class="ri-medal-fill" style="color:#8D6E63; font-size:20px"></i>
|
||||||
|
<div style="font-size:10px">8</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rules Card -->
|
||||||
|
<div class="info-card pixel-border" id="rules-card">
|
||||||
|
<div class="info-header">
|
||||||
|
<span>RULES</span>
|
||||||
|
<i class="ri-book-open-fill toggle-btn" onclick="toggleRules()"></i>
|
||||||
|
</div>
|
||||||
|
<div class="collapsible-content" id="rules-content">
|
||||||
|
<ul style="padding-left: 20px; font-size: 12px; line-height: 1.8; color:#ccc;">
|
||||||
|
<li>Duration: 3 Hours</li>
|
||||||
|
<li>Penalty: +5 mins per WA</li>
|
||||||
|
<li>Languages: Java, C++, Python</li>
|
||||||
|
<li>Plagiarism check is ACTIVE</li>
|
||||||
|
<li>Do not break obsidian blocks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Past Results -->
|
||||||
|
<div class="info-card pixel-border">
|
||||||
|
<div class="info-header">
|
||||||
|
<span>HISTORY</span>
|
||||||
|
<i class="ri-history-line"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="timeline-date">2023-10-15</div>
|
||||||
|
<div class="timeline-rank">Weekly #41 - Rank #15</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot" style="background:#BDBDBD"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="timeline-date">2023-10-08</div>
|
||||||
|
<div class="timeline-rank">Weekly #40 - Rank #2 🥈</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot" style="background:#8D6E63"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="timeline-date">2023-10-01</div>
|
||||||
|
<div class="timeline-rank">Weekly #39 - Rank #3 🥉</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-line"></div>
|
||||||
|
<div class="timeline-dot" style="background:#555"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="timeline-date">2023-09-24</div>
|
||||||
|
<div class="timeline-rank">Weekly #38 - Rank #45</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- Countdown Timer ---
|
||||||
|
let totalSeconds = 2 * 3600 + 45 * 60 + 30; // 2h 45m 30s
|
||||||
|
const countdownEl = document.getElementById('countdown');
|
||||||
|
|
||||||
|
function updateCountdown() {
|
||||||
|
const h = Math.floor(totalSeconds / 3600);
|
||||||
|
const m = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const s = totalSeconds % 60;
|
||||||
|
|
||||||
|
countdownEl.textContent =
|
||||||
|
`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
|
if (totalSeconds > 0) {
|
||||||
|
totalSeconds--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(updateCountdown, 1000);
|
||||||
|
updateCountdown();
|
||||||
|
|
||||||
|
// --- Tabs Functionality ---
|
||||||
|
function switchTab(tabName) {
|
||||||
|
const tabs = document.querySelectorAll('.tab');
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
// Find the clicked tab (simple logic for this demo)
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
// In a real app, this would filter the card list
|
||||||
|
const list = document.getElementById('contest-list');
|
||||||
|
list.style.opacity = '0.5';
|
||||||
|
setTimeout(() => {
|
||||||
|
list.style.opacity = '1';
|
||||||
|
// Mock content change
|
||||||
|
if(tabName === 'finished') {
|
||||||
|
// Just a visual cue that something changed
|
||||||
|
list.innerHTML = `<div style="text-align:center; padding:40px; color:#aaa">Loading Archive...</div>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
list.innerHTML = `
|
||||||
|
<div class="contest-card pixel-border" style="opacity:0.7">
|
||||||
|
<div class="flex-center">
|
||||||
|
<div class="card-icon pixel-border" style="background:#555; color:#aaa"><i class="ri-trophy-line"></i></div>
|
||||||
|
<div class="card-info">
|
||||||
|
<h3 style="color:#aaa">Weekly Challenge #41</h3>
|
||||||
|
<div class="card-details">
|
||||||
|
<span>Ended: 2 days ago</span>
|
||||||
|
<span>Winner: Herobrine</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-action">
|
||||||
|
<button class="btn-small">VIEW</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}, 500);
|
||||||
|
} else if(tabName === 'ongoing') {
|
||||||
|
// Reset to initial HTML (simplified)
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Toggle Rules ---
|
||||||
|
function toggleRules() {
|
||||||
|
const card = document.getElementById('rules-card');
|
||||||
|
card.classList.toggle('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Real-time Leaderboard Simulation ---
|
||||||
|
function simulateLeaderboard() {
|
||||||
|
const rows = document.querySelectorAll('#leaderboard-body tr:not(.current-user)');
|
||||||
|
// Randomly swap two rows to simulate rank changes
|
||||||
|
const idx1 = Math.floor(Math.random() * 3); // Only top 3 change mostly
|
||||||
|
const idx2 = Math.floor(Math.random() * 3);
|
||||||
|
|
||||||
|
if (idx1 !== idx2) {
|
||||||
|
// Flash effect
|
||||||
|
rows[idx1].style.backgroundColor = '#fff';
|
||||||
|
setTimeout(() => {
|
||||||
|
rows[idx1].style.backgroundColor = ''; // revert to CSS class style
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(simulateLeaderboard, 10000);
|
||||||
|
|
||||||
|
// --- LocalStorage Logic ---
|
||||||
|
// Save visit timestamp
|
||||||
|
localStorage.setItem('last_visit_mc_contest', new Date().toISOString());
|
||||||
|
|
||||||
|
// --- Firework/Hover Effects for Top 3 ---
|
||||||
|
const topRanks = document.querySelectorAll('.rank-1, .rank-2, .rank-3');
|
||||||
|
topRanks.forEach(row => {
|
||||||
|
row.addEventListener('mouseenter', () => {
|
||||||
|
// Add a subtle scale effect
|
||||||
|
row.style.transform = "scale(1.02)";
|
||||||
|
row.style.transition = "transform 0.2s";
|
||||||
|
row.style.zIndex = "10";
|
||||||
|
row.style.boxShadow = "0 0 15px rgba(255, 215, 0, 0.5)";
|
||||||
|
});
|
||||||
|
row.addEventListener('mouseleave', () => {
|
||||||
|
row.style.transform = "scale(1)";
|
||||||
|
row.style.boxShadow = "none";
|
||||||
|
row.style.zIndex = "1";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,605 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CSP Learning Platform - Minecraft Login</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--mc-dirt-side: #79553a;
|
||||||
|
--mc-dirt-top: #5c3f2b;
|
||||||
|
--mc-grass-top: #70b348;
|
||||||
|
--mc-grass-side: #5a9139;
|
||||||
|
--mc-stone: #7d7d7d;
|
||||||
|
--mc-wood: #a0744b;
|
||||||
|
--mc-wood-dark: #6e4e34;
|
||||||
|
--mc-wood-light: #bca07e;
|
||||||
|
--mc-diamond: #29B6F6;
|
||||||
|
--mc-redstone: #E53935;
|
||||||
|
--mc-gold: #FFD700;
|
||||||
|
--mc-sky: #cceeff;
|
||||||
|
--bg-sky-top: #87CEEB;
|
||||||
|
--bg-sky-bottom: #E0F7FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 1920px;
|
||||||
|
min-height: 1080px;
|
||||||
|
background: linear-gradient(to bottom, var(--bg-sky-top), var(--bg-sky-bottom));
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pixel Cloud Background Decoration */
|
||||||
|
.clouds {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 15% 20%, white 20px, transparent 21px),
|
||||||
|
radial-gradient(circle at 85% 30%, white 30px, transparent 31px),
|
||||||
|
radial-gradient(circle at 50% 10%, rgba(255,255,255,0.8) 40px, transparent 41px);
|
||||||
|
background-size: 800px 400px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Nav */
|
||||||
|
.navbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 40px;
|
||||||
|
border-bottom: 4px solid rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 4px 4px 0px #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo i {
|
||||||
|
color: var(--mc-grass-top);
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Container */
|
||||||
|
.main-container {
|
||||||
|
margin-top: 140px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wooden Card */
|
||||||
|
.login-card {
|
||||||
|
width: 480px;
|
||||||
|
background-color: var(--mc-wood);
|
||||||
|
border: 4px solid #000;
|
||||||
|
box-shadow:
|
||||||
|
8px 8px 0px rgba(0,0,0,0.5),
|
||||||
|
inset 4px 4px 0px var(--mc-wood-light),
|
||||||
|
inset -4px -4px 0px var(--mc-wood-dark);
|
||||||
|
padding: 32px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wood Grain Texture CSS Pattern */
|
||||||
|
.login-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(45deg,
|
||||||
|
rgba(0,0,0,0.05) 0px,
|
||||||
|
rgba(0,0,0,0.05) 2px,
|
||||||
|
transparent 2px,
|
||||||
|
transparent 8px
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-layer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 4px solid var(--mc-wood-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-top: 4px solid var(--mc-wood-light);
|
||||||
|
border-left: 4px solid var(--mc-wood-light);
|
||||||
|
border-right: 4px solid var(--mc-wood-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Elements */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 16px 16px 48px;
|
||||||
|
background-color: #333;
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-right-color: #555;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 4px 4px 0px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #333;
|
||||||
|
border: 2px solid #000;
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: inset 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-checkbox i {
|
||||||
|
display: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--mc-grass-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group.checked .custom-checkbox i {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Password Strength */
|
||||||
|
.strength-meter {
|
||||||
|
height: 8px;
|
||||||
|
background-color: #000;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s, background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-text {
|
||||||
|
font-size: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
text-align: right;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.btn-minecraft {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--mc-grass-top);
|
||||||
|
border: 4px solid #000;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-shadow: 2px 2px 0px #000;
|
||||||
|
box-shadow:
|
||||||
|
inset 4px 4px 0px rgba(255,255,255,0.3),
|
||||||
|
inset -4px -4px 0px rgba(0,0,0,0.3),
|
||||||
|
4px 4px 0px #000;
|
||||||
|
transition: transform 0.1s, box-shadow 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-minecraft:active {
|
||||||
|
transform: translate(4px, 4px);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-minecraft:hover {
|
||||||
|
background-color: #7bc453;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
.links {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-diamond {
|
||||||
|
color: var(--mc-diamond);
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-diamond:hover {
|
||||||
|
border-bottom-color: var(--mc-diamond);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error Message */
|
||||||
|
.error-msg {
|
||||||
|
color: var(--mc-redstone);
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating XP Orbs */
|
||||||
|
.xp-orb {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #bcfc03;
|
||||||
|
border: 2px solid #6c8f02;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
box-shadow: 0 0 10px #bcfc03;
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-1 { top: -40px; left: -40px; animation-delay: 0s; background-color: #bcfc03; }
|
||||||
|
.orb-2 { bottom: -30px; right: -30px; animation-delay: 1.5s; background-color: #64ffda; border-color: #009688; }
|
||||||
|
.orb-3 { top: 50%; right: -60px; animation-delay: 0.8s; width: 16px; height: 16px; }
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: rotate(45deg) translateY(0); }
|
||||||
|
50% { transform: rotate(45deg) translateY(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background-color: rgba(0,0,0,0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 4px solid rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-players {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-head {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: #333;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-head:nth-child(2) { background-color: #AA0000; }
|
||||||
|
.player-head:nth-child(3) { background-color: #00AA00; }
|
||||||
|
.player-head:nth-child(4) { background-color: #0000AA; }
|
||||||
|
|
||||||
|
/* Grass Block Floor Decoration */
|
||||||
|
.grass-floor {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, var(--mc-grass-side) 50%, var(--mc-grass-top) 50%),
|
||||||
|
linear-gradient(to bottom, transparent 50%, var(--mc-dirt-side) 50%);
|
||||||
|
background-size: 40px 40px, 100% 40px;
|
||||||
|
z-index: 5;
|
||||||
|
display: none; /* Hidden to keep clean look, or show for immersion */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="logo">
|
||||||
|
<i class="ri-box-3-fill"></i>
|
||||||
|
CSP Learning Platform
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="clouds"></div>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="login-card">
|
||||||
|
<!-- Decorative Orbs -->
|
||||||
|
<div class="xp-orb orb-1"></div>
|
||||||
|
<div class="xp-orb orb-2"></div>
|
||||||
|
<div class="xp-orb orb-3"></div>
|
||||||
|
|
||||||
|
<div class="content-layer">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('login')">Login</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('register')">Register</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="authForm" onsubmit="handleLogin(event)">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<i class="ri-user-smile-fill input-icon"></i>
|
||||||
|
<input type="text" id="username" placeholder="Username" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div id="username-error" class="error-msg">Username must be at least 3 chars!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<i class="ri-lock-fill input-icon"></i>
|
||||||
|
<input type="password" id="password" placeholder="Password" oninput="checkStrength()">
|
||||||
|
<i class="ri-eye-off-fill toggle-password" onclick="togglePassword()"></i>
|
||||||
|
</div>
|
||||||
|
<div class="strength-meter">
|
||||||
|
<div class="strength-bar" id="strength-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="strength-text" id="strength-text"></div>
|
||||||
|
<div id="password-error" class="error-msg">Password must be at least 6 chars!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-group" onclick="toggleRemember(this)">
|
||||||
|
<div class="custom-checkbox">
|
||||||
|
<i class="ri-check-fill"></i>
|
||||||
|
</div>
|
||||||
|
<span>Remember me</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-minecraft" id="submit-btn">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<a href="#" class="link-diamond">Forgot password?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="online-players">
|
||||||
|
<span>Online Players: <span style="color: var(--mc-gold);">12,002</span></span>
|
||||||
|
<div class="player-head"></div>
|
||||||
|
<div class="player-head"></div>
|
||||||
|
<div class="player-head"></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let isLogin = true;
|
||||||
|
let rememberMe = false;
|
||||||
|
|
||||||
|
function switchTab(tab) {
|
||||||
|
const btns = document.querySelectorAll('.tab-btn');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const form = document.getElementById('authForm');
|
||||||
|
|
||||||
|
// Clear errors
|
||||||
|
document.querySelectorAll('.error-msg').forEach(el => el.style.display = 'none');
|
||||||
|
|
||||||
|
if (tab === 'login') {
|
||||||
|
isLogin = true;
|
||||||
|
btns[0].classList.add('active');
|
||||||
|
btns[1].classList.remove('active');
|
||||||
|
submitBtn.innerText = 'Login';
|
||||||
|
} else {
|
||||||
|
isLogin = false;
|
||||||
|
btns[0].classList.remove('active');
|
||||||
|
btns[1].classList.add('active');
|
||||||
|
submitBtn.innerText = 'Register';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePassword() {
|
||||||
|
const pwdInput = document.getElementById('password');
|
||||||
|
const icon = document.querySelector('.toggle-password');
|
||||||
|
if (pwdInput.type === 'password') {
|
||||||
|
pwdInput.type = 'text';
|
||||||
|
icon.classList.remove('ri-eye-off-fill');
|
||||||
|
icon.classList.add('ri-eye-fill');
|
||||||
|
} else {
|
||||||
|
pwdInput.type = 'password';
|
||||||
|
icon.classList.remove('ri-eye-fill');
|
||||||
|
icon.classList.add('ri-eye-off-fill');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRemember(el) {
|
||||||
|
el.classList.toggle('checked');
|
||||||
|
rememberMe = !rememberMe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkStrength() {
|
||||||
|
const pwd = document.getElementById('password').value;
|
||||||
|
const bar = document.getElementById('strength-bar');
|
||||||
|
const text = document.getElementById('strength-text');
|
||||||
|
|
||||||
|
if (pwd.length === 0) {
|
||||||
|
bar.style.width = '0%';
|
||||||
|
bar.style.backgroundColor = 'transparent';
|
||||||
|
text.innerText = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let strength = 0;
|
||||||
|
if (pwd.length >= 6) strength++;
|
||||||
|
if (pwd.match(/[a-z]/) && pwd.match(/[0-9]/)) strength++;
|
||||||
|
if (pwd.length > 8 && pwd.match(/[^a-zA-Z0-9]/)) strength++;
|
||||||
|
|
||||||
|
if (strength === 1) {
|
||||||
|
bar.style.width = '33%';
|
||||||
|
bar.style.backgroundColor = '#E53935'; // Weak
|
||||||
|
text.innerText = 'Weak';
|
||||||
|
text.style.color = '#E53935';
|
||||||
|
} else if (strength === 2) {
|
||||||
|
bar.style.width = '66%';
|
||||||
|
bar.style.backgroundColor = '#FFD700'; // Medium
|
||||||
|
text.innerText = 'Medium';
|
||||||
|
text.style.color = '#FFD700';
|
||||||
|
} else {
|
||||||
|
bar.style.width = '100%';
|
||||||
|
bar.style.backgroundColor = '#7CB342'; // Strong
|
||||||
|
text.innerText = 'Strong';
|
||||||
|
text.style.color = '#7CB342';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogin(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = document.getElementById('username').value;
|
||||||
|
const pass = document.getElementById('password').value;
|
||||||
|
const userErr = document.getElementById('username-error');
|
||||||
|
const passErr = document.getElementById('password-error');
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Reset errors
|
||||||
|
userErr.style.display = 'none';
|
||||||
|
passErr.style.display = 'none';
|
||||||
|
|
||||||
|
if (user.length < 3) {
|
||||||
|
userErr.style.display = 'block';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (pass.length < 6) {
|
||||||
|
passErr.style.display = 'block';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) return;
|
||||||
|
|
||||||
|
// Mock login check
|
||||||
|
if (isLogin) {
|
||||||
|
if (user === 'test' && pass === 'whoami139') {
|
||||||
|
// Success
|
||||||
|
const token = 'mock_token_' + Date.now();
|
||||||
|
localStorage.setItem('authToken', token);
|
||||||
|
if(rememberMe) localStorage.setItem('savedUser', user);
|
||||||
|
|
||||||
|
// Simulate redirect with a nice alert or console log since we are in a static file context
|
||||||
|
// In a real app: window.location.href = 'problem-library.html';
|
||||||
|
const btn = document.getElementById('submit-btn');
|
||||||
|
btn.innerText = 'Connecting...';
|
||||||
|
btn.style.backgroundColor = '#FFD700'; // Gold color loading
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// We just reload or show success for this demo
|
||||||
|
btn.innerText = 'Success!';
|
||||||
|
btn.style.backgroundColor = '#7CB342';
|
||||||
|
// Simulating redirect
|
||||||
|
// window.location.href = 'problem-library.html';
|
||||||
|
console.log("Redirecting to problem-library.html");
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// Wrong credentials
|
||||||
|
// Create a generic error message for demo
|
||||||
|
passErr.innerText = 'Invalid username or password!';
|
||||||
|
passErr.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Register logic mock
|
||||||
|
alert(`Registered user: ${user}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,849 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Minecraft Problem Library</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Font Loading */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Press Start 2P';
|
||||||
|
src: url('https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6jDQyK3nVivM.woff2') format('woff2');
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'MiSans-Regular';
|
||||||
|
src: url('https://assets-persist.lovart.ai/agent-static-assets/MiSans-Regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mc-dirt-dark: #5e4032;
|
||||||
|
--mc-dirt-light: #866043;
|
||||||
|
--mc-grass-side: #7CB342;
|
||||||
|
--mc-grass-top: #558B2F;
|
||||||
|
--mc-stone: #757575;
|
||||||
|
--mc-stone-light: #9E9E9E;
|
||||||
|
--mc-wood-dark: #4E342E;
|
||||||
|
--mc-wood-light: #795548;
|
||||||
|
--mc-plank: #A1887F;
|
||||||
|
--mc-text-shadow: 2px 2px 0 #000;
|
||||||
|
--mc-bg: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'MiSans-Regular', sans-serif;
|
||||||
|
background-color: #121212;
|
||||||
|
color: white;
|
||||||
|
width: 1920px;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
|
||||||
|
url('https://images.unsplash.com/photo-1587573088697-b4f9d17102dd?q=80&w=2669&auto=format&fit=crop'); /* Minecraft-like texture bg */
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minecraft Button Utility */
|
||||||
|
.mc-btn {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: 4px solid #000;
|
||||||
|
background: #9E9E9E;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px #333;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: inset -4px -4px 0px #555, inset 4px 4px 0px #DDD;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-btn:active {
|
||||||
|
box-shadow: inset -4px -4px 0px #DDD, inset 4px 4px 0px #555;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-btn.green {
|
||||||
|
background: #7CB342;
|
||||||
|
box-shadow: inset -4px -4px 0px #33691E, inset 4px 4px 0px #AED581;
|
||||||
|
}
|
||||||
|
.mc-btn.green:active {
|
||||||
|
box-shadow: inset -4px -4px 0px #AED581, inset 4px 4px 0px #33691E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Navigation */
|
||||||
|
.top-nav {
|
||||||
|
height: 70px;
|
||||||
|
background-color: rgba(0,0,0,0.8);
|
||||||
|
border-bottom: 4px solid #3e3e3e;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 40px;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #FFEB3B;
|
||||||
|
text-shadow: var(--mc-text-shadow);
|
||||||
|
margin-right: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover, .nav-item.active {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 2px solid #fff;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Container */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 1920px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: #5D4037; /* Dark Wood */
|
||||||
|
border-right: 4px solid #3E2723;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
background-image: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255,255,255,0.05) 0px,
|
||||||
|
rgba(255,255,255,0.05) 2px,
|
||||||
|
transparent 2px,
|
||||||
|
transparent 10px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link {
|
||||||
|
display: block;
|
||||||
|
padding: 15px;
|
||||||
|
background: #8D6E63; /* Plank color */
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
border: 4px solid #3E2723;
|
||||||
|
box-shadow: inset 2px 2px 0 rgba(255,255,255,0.2);
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
background: #A1887F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link.active {
|
||||||
|
background: #7CB342;
|
||||||
|
border-color: #33691E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-quest-panel {
|
||||||
|
background: #263238;
|
||||||
|
border: 4px solid #000;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-title {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #FFEB3B;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 1px 1px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-progress {
|
||||||
|
height: 20px;
|
||||||
|
background: #37474F;
|
||||||
|
border: 2px solid #000;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-bar {
|
||||||
|
height: 100%;
|
||||||
|
width: 60%;
|
||||||
|
background: #29B6F6;
|
||||||
|
box-shadow: inset 0 -2px 0 rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quest-text {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: right;
|
||||||
|
color: #B0BEC5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 32px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 4px 4px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Cards */
|
||||||
|
.stats-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: #546E7A; /* Stone Blue */
|
||||||
|
border: 4px solid #263238;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 6px 6px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
right: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
border: 2px dashed rgba(255,255,255,0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #CFD8DC;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters */
|
||||||
|
.filter-section {
|
||||||
|
background: #3E2723;
|
||||||
|
border: 4px solid #000;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #5D4037;
|
||||||
|
border: 2px solid #8D6E63;
|
||||||
|
color: #D7CCC8;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #7CB342;
|
||||||
|
color: white;
|
||||||
|
border-color: #AED581;
|
||||||
|
box-shadow: 0 4px 0 #33691E;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-select {
|
||||||
|
font-family: 'MiSans-Regular', sans-serif; /* Readable font for dropdown */
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #D7CCC8;
|
||||||
|
border: 3px solid #5D4037;
|
||||||
|
color: #3E2723;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-input {
|
||||||
|
font-family: 'MiSans-Regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #212121;
|
||||||
|
border: 3px solid #616161;
|
||||||
|
color: #fff;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-search-btn {
|
||||||
|
background: #FFB74D;
|
||||||
|
border: 3px solid #E65100;
|
||||||
|
color: #3E2723;
|
||||||
|
padding: 0 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem Table */
|
||||||
|
.problem-table-container {
|
||||||
|
background: #8D6E63; /* Plank */
|
||||||
|
border: 4px solid #3E2723;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 8px 8px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table th {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 15px;
|
||||||
|
background: #5D4037;
|
||||||
|
color: #FFCC80;
|
||||||
|
border-bottom: 4px solid #3E2723;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 2px solid #5D4037;
|
||||||
|
color: #3E2723;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table tr:hover td {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.status-solved { color: #2E7D32; }
|
||||||
|
.status-locked { color: #616161; }
|
||||||
|
.status-attempted { color: #F57F17; }
|
||||||
|
|
||||||
|
.difficulty-stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.star {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-badge {
|
||||||
|
background: #3E2723;
|
||||||
|
color: #D7CCC8;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-btn {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background: #FFB74D;
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: inset -2px -2px 0 #E65100, inset 2px 2px 0 #FFE0B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-btn:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-info {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Floating Panel */
|
||||||
|
.right-panel {
|
||||||
|
width: 260px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
background: #F5F5F5; /* Paper */
|
||||||
|
border: 4px solid #424242;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
|
||||||
|
position: sticky;
|
||||||
|
top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 10px;
|
||||||
|
background: #B71C1C; /* Red tape */
|
||||||
|
width: 40px;
|
||||||
|
margin: -25px auto 15px;
|
||||||
|
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #212121;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px dashed #9E9E9E;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-checkbox {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-checkbox:checked {
|
||||||
|
background: #7CB342;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-checkbox:checked::after {
|
||||||
|
content: '✔';
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: 1px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-reward {
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #F57F17;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stats-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="top-nav">
|
||||||
|
<div class="nav-brand">
|
||||||
|
<i class="ri-book-3-fill" style="color: #8D6E63; font-size: 24px;"></i>
|
||||||
|
CRAFT CODE
|
||||||
|
</div>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="#" class="nav-item">HOME</a>
|
||||||
|
<a href="#" class="nav-item active">PROBLEMS</a>
|
||||||
|
<a href="#" class="nav-item">CONTEST</a>
|
||||||
|
<a href="#" class="nav-item">LEADERBOARD</a>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=Felix" alt="User" class="user-avatar">
|
||||||
|
<div style="font-family: 'Press Start 2P'; font-size: 10px; color: #fff;">Steve</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<ul class="sidebar-menu">
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link active">ALL PROBLEMS</a></li>
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link">ALGORITHMS</a></li>
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link">DATA STRUCTURES</a></li>
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link">MATH</a></li>
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link">STRINGS</a></li>
|
||||||
|
<li class="menu-item"><a href="#" class="menu-link">DYNAMIC PROG</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="daily-quest-panel">
|
||||||
|
<div class="quest-title">DAILY QUEST</div>
|
||||||
|
<div class="quest-progress">
|
||||||
|
<div class="quest-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="quest-text">3/5 COMPLETED</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="page-header">
|
||||||
|
<i class="ri-book-read-line" style="font-size: 32px; color: #FFD54F;"></i>
|
||||||
|
<h1 class="page-title">PROBLEM LIBRARY</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">TOTAL PROBLEMS</div>
|
||||||
|
<div class="stat-value">1,556</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">COMPLETED</div>
|
||||||
|
<div class="stat-value" style="color: #7CB342;">128</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">PASS RATE</div>
|
||||||
|
<div class="stat-value" style="color: #FFB74D;">85%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="category-tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab(this)">CSP-J</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab(this)">CSP-S</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab(this)">NOIP</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-controls">
|
||||||
|
<select class="mc-select">
|
||||||
|
<option>All Difficulty</option>
|
||||||
|
<option>1 Star</option>
|
||||||
|
<option>2 Stars</option>
|
||||||
|
<option>3 Stars</option>
|
||||||
|
<option>4 Stars</option>
|
||||||
|
<option>5 Stars</option>
|
||||||
|
</select>
|
||||||
|
<select class="mc-select">
|
||||||
|
<option>All Tags</option>
|
||||||
|
<option>Array</option>
|
||||||
|
<option>Stack</option>
|
||||||
|
<option>Queue</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="mc-input" id="searchInput" placeholder="Search problems...">
|
||||||
|
<button class="mc-search-btn" onclick="filterTable()">
|
||||||
|
<i class="ri-search-2-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="problem-table-container">
|
||||||
|
<table class="problem-table" id="problemTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50">STS</th>
|
||||||
|
<th width="60">ID</th>
|
||||||
|
<th>TITLE</th>
|
||||||
|
<th width="100">RATE</th>
|
||||||
|
<th width="100">DIFF</th>
|
||||||
|
<th width="150">TAGS</th>
|
||||||
|
<th width="80">ACT</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Data rows will be populated by JS -->
|
||||||
|
<tr>
|
||||||
|
<td><i class="ri-checkbox-circle-fill status-solved status-icon"></i></td>
|
||||||
|
<td>1001</td>
|
||||||
|
<td>A+B Problem</td>
|
||||||
|
<td>95%</td>
|
||||||
|
<td>
|
||||||
|
<div class="difficulty-stars">
|
||||||
|
<i class="ri-star-fill star" style="color:#7CB342"></i>
|
||||||
|
<i class="ri-star-line star" style="color:#757575"></i>
|
||||||
|
<i class="ri-star-line star" style="color:#757575"></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="tag-badge">Math</span></td>
|
||||||
|
<td><a href="#" class="table-btn">SOLVE</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="ri-lock-fill status-locked status-icon"></i></td>
|
||||||
|
<td>1002</td>
|
||||||
|
<td>Fibonacci Sequence</td>
|
||||||
|
<td>45%</td>
|
||||||
|
<td>
|
||||||
|
<div class="difficulty-stars">
|
||||||
|
<i class="ri-star-fill star" style="color:#FFB74D"></i>
|
||||||
|
<i class="ri-star-fill star" style="color:#FFB74D"></i>
|
||||||
|
<i class="ri-star-line star" style="color:#757575"></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="tag-badge">DP</span></td>
|
||||||
|
<td><a href="#" class="table-btn">SOLVE</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="mc-btn">PREV</button>
|
||||||
|
<div class="page-info">PAGE 1 / 32</div>
|
||||||
|
<button class="mc-btn green">NEXT</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Right Panel -->
|
||||||
|
<aside class="right-panel">
|
||||||
|
<div class="task-card">
|
||||||
|
<div class="task-header">DAILY TASKS</div>
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="mc-checkbox" checked>
|
||||||
|
<span>Login Daily</span>
|
||||||
|
<span class="xp-reward">+10XP</span>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="mc-checkbox" checked>
|
||||||
|
<span>Solve 1 Easy</span>
|
||||||
|
<span class="xp-reward">+20XP</span>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="mc-checkbox">
|
||||||
|
<span>Solve 1 Medium</span>
|
||||||
|
<span class="xp-reward">+50XP</span>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="mc-checkbox">
|
||||||
|
<span>Submit 5 times</span>
|
||||||
|
<span class="xp-reward">+15XP</span>
|
||||||
|
</li>
|
||||||
|
<li class="task-item">
|
||||||
|
<input type="checkbox" class="mc-checkbox">
|
||||||
|
<span>Review Code</span>
|
||||||
|
<span class="xp-reward">+30XP</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Sample Data Generation
|
||||||
|
const problems = [
|
||||||
|
{ id: '1001', title: 'A+B Problem', rate: '98%', diff: 1, tags: ['Math'], status: 'solved' },
|
||||||
|
{ id: '1002', title: 'Knapsack Problem', rate: '45%', diff: 3, tags: ['DP', 'Math'], status: 'attempted' },
|
||||||
|
{ id: '1003', title: 'Binary Tree Traversal', rate: '60%', diff: 2, tags: ['Tree', 'DFS'], status: 'locked' },
|
||||||
|
{ id: '1004', title: 'Shortest Path (Dijkstra)', rate: '32%', diff: 4, tags: ['Graph'], status: 'locked' },
|
||||||
|
{ id: '1005', title: 'String Matching (KMP)', rate: '28%', diff: 5, tags: ['String'], status: 'locked' },
|
||||||
|
{ id: '1006', title: 'Counting Sort', rate: '75%', diff: 2, tags: ['Sort'], status: 'solved' },
|
||||||
|
{ id: '1007', title: 'N-Queens', rate: '40%', diff: 3, tags: ['Backtrack'], status: 'locked' },
|
||||||
|
{ id: '1008', title: 'Union Find', rate: '55%', diff: 3, tags: ['DS'], status: 'attempted' },
|
||||||
|
{ id: '1009', title: 'Segment Tree Range Sum', rate: '20%', diff: 5, tags: ['Tree'], status: 'locked' },
|
||||||
|
{ id: '1010', title: 'Prim MST', rate: '35%', diff: 4, tags: ['Graph'], status: 'locked' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function getStatusIcon(status) {
|
||||||
|
if (status === 'solved') return '<i class="ri-checkbox-circle-fill status-solved status-icon"></i>';
|
||||||
|
if (status === 'attempted') return '<i class="ri-record-circle-fill status-attempted status-icon"></i>';
|
||||||
|
return '<i class="ri-lock-fill status-locked status-icon"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStars(count) {
|
||||||
|
let html = '<div class="difficulty-stars">';
|
||||||
|
const colors = ['#7CB342', '#29B6F6', '#FFB74D', '#FF7043', '#D32F2F']; // Color coding
|
||||||
|
const color = colors[count - 1] || '#757575';
|
||||||
|
|
||||||
|
for(let i=0; i<5; i++) {
|
||||||
|
if(i < count) {
|
||||||
|
html += `<i class="ri-star-fill star" style="color:${color}"></i>`;
|
||||||
|
} else {
|
||||||
|
html += `<i class="ri-star-line star" style="color:#757575"></i>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.querySelector('#problemTable tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
data.forEach(p => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${getStatusIcon(p.status)}</td>
|
||||||
|
<td>${p.id}</td>
|
||||||
|
<td style="cursor:pointer;" onclick="window.location.href='code-editor.html?id=${p.id}'">${p.title}</td>
|
||||||
|
<td>${p.rate}</td>
|
||||||
|
<td>${getStars(p.diff)}</td>
|
||||||
|
<td>${p.tags.map(t => `<span class="tag-badge">${t}</span>`).join('')}</td>
|
||||||
|
<td><a href="code-editor.html?id=${p.id}" class="table-btn">SOLVE</a></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab Switching Logic
|
||||||
|
function switchTab(element) {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
element.classList.add('active');
|
||||||
|
// Mock data change
|
||||||
|
const shuffled = [...problems].sort(() => 0.5 - Math.random());
|
||||||
|
renderTable(shuffled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Logic
|
||||||
|
function filterTable() {
|
||||||
|
const term = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
const filtered = problems.filter(p =>
|
||||||
|
p.title.toLowerCase().includes(term) ||
|
||||||
|
p.id.includes(term) ||
|
||||||
|
p.tags.some(t => t.toLowerCase().includes(term))
|
||||||
|
);
|
||||||
|
renderTable(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real-time search
|
||||||
|
document.getElementById('searchInput').addEventListener('input', filterTable);
|
||||||
|
|
||||||
|
// Sidebar Active State
|
||||||
|
document.querySelectorAll('.menu-link').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelectorAll('.menu-link').forEach(l => l.classList.remove('active'));
|
||||||
|
link.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
renderTable(problems);
|
||||||
|
|
||||||
|
// Save checkbox state to localStorage
|
||||||
|
document.querySelectorAll('.mc-checkbox').forEach((box, index) => {
|
||||||
|
const saved = localStorage.getItem(`task-${index}`);
|
||||||
|
if(saved !== null) {
|
||||||
|
box.checked = saved === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
box.addEventListener('change', () => {
|
||||||
|
localStorage.setItem(`task-${index}`, box.checked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,896 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Minecraft Profile Center</title>
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #1a1a1a;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--minecraft-green: #59a228;
|
||||||
|
--minecraft-dark-green: #376318;
|
||||||
|
--wood-light: #a07449;
|
||||||
|
--wood-dark: #634329;
|
||||||
|
--wood-border: #3d2919;
|
||||||
|
--gold: #fcc201;
|
||||||
|
--purple: #aa00aa;
|
||||||
|
--iron: #c6c6c6;
|
||||||
|
--diamond: #3fd2ea;
|
||||||
|
--red: #aa0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #252525 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, #252525 25%, transparent 25%),
|
||||||
|
linear-gradient(45deg, transparent 75%, #252525 75%),
|
||||||
|
linear-gradient(-45deg, transparent 75%, #252525 75%);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
font-family: 'Press Start 2P', cursive;
|
||||||
|
color: var(--text-color);
|
||||||
|
width: 1920px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.flex { display: flex; }
|
||||||
|
.flex-col { flex-direction: column; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.gap-2 { gap: 8px; }
|
||||||
|
.gap-4 { gap: 16px; }
|
||||||
|
.w-full { width: 100%; }
|
||||||
|
.h-full { height: 100%; }
|
||||||
|
|
||||||
|
/* Minecraft UI Components */
|
||||||
|
.mc-btn {
|
||||||
|
background-color: #7d7d7d;
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-top-color: #dedede;
|
||||||
|
border-left-color: #dedede;
|
||||||
|
border-right-color: #555;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-btn:active, .mc-btn.active {
|
||||||
|
background-color: #555;
|
||||||
|
border-top-color: #333;
|
||||||
|
border-left-color: #333;
|
||||||
|
border-right-color: #dedede;
|
||||||
|
border-bottom-color: #dedede;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card {
|
||||||
|
background-color: #c6c6c6;
|
||||||
|
border: 4px solid #000;
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-left-color: #fff;
|
||||||
|
border-right-color: #555;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
padding: 4px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-card-inner {
|
||||||
|
background-color: #8b8b8b;
|
||||||
|
border: 4px solid #373737;
|
||||||
|
border-right-color: #fff;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-wood-panel {
|
||||||
|
background-color: var(--wood-light);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgba(0,0,0,0.1) 50%, transparent 50%),
|
||||||
|
linear-gradient(rgba(0,0,0,0.1) 50%, transparent 50%);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
border: 4px solid var(--wood-border);
|
||||||
|
box-shadow: inset 4px 4px 0 var(--wood-dark), inset -4px -4px 0 var(--wood-dark);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top Nav */
|
||||||
|
nav {
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
padding: 20px 40px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
border-bottom: 4px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand { font-size: 24px; color: var(--diamond); text-shadow: 2px 2px #000; }
|
||||||
|
.nav-links { gap: 20px; }
|
||||||
|
|
||||||
|
/* Main Container */
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Info Card */
|
||||||
|
.user-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr 400px;
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
background: #000;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
text-shadow: 3px 3px 0 #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-lv {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--gold);
|
||||||
|
color: #000;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
margin-right: 10px;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-banner {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--purple);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid #d369d3;
|
||||||
|
text-shadow: 2px 2px #000;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
background: #333;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
margin-top: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--minecraft-green);
|
||||||
|
width: 0%; /* JS will animate */
|
||||||
|
transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-bar-fill::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(255, 255, 255, 0.2) 25%,
|
||||||
|
transparent 25%,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(255, 255, 255, 0.2) 50%,
|
||||||
|
rgba(255, 255, 255, 0.2) 75%,
|
||||||
|
transparent 75%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xp-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 1px 1px #000;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 2px solid rgba(255,255,255,0.2);
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-val { font-size: 20px; color: var(--gold); margin-bottom: 5px; }
|
||||||
|
.stat-label { font-size: 10px; color: #ccc; }
|
||||||
|
|
||||||
|
/* 3 Columns Layout */
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 25% 50% 25%;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column Styles */
|
||||||
|
.col-card {
|
||||||
|
background: #c6c6c6;
|
||||||
|
border: 4px solid #555;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.col-card-inner {
|
||||||
|
background: #222;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 15px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fff;
|
||||||
|
border-bottom: 2px solid #555;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pie Chart Simulation */
|
||||||
|
.pie-chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
margin: 20px auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(
|
||||||
|
var(--minecraft-green) 0% 50%,
|
||||||
|
var(--gold) 50% 80%,
|
||||||
|
var(--red) 80% 100%
|
||||||
|
);
|
||||||
|
border: 4px solid #000;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Grid */
|
||||||
|
.activity-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.activity-cell {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 100%;
|
||||||
|
background: #333;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.activity-cell[data-level="1"] { background: #1a4d1a; }
|
||||||
|
.activity-cell[data-level="2"] { background: #2b7a2b; }
|
||||||
|
.activity-cell[data-level="3"] { background: #40b340; }
|
||||||
|
.activity-cell[data-level="4"] { background: #5cd65c; }
|
||||||
|
|
||||||
|
/* Achievement Wall */
|
||||||
|
.achievement-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
background: url('https://www.transparenttextures.com/patterns/wood-pattern.png') #634329;
|
||||||
|
border: 8px solid #3d2919;
|
||||||
|
box-shadow: inset 0 0 20px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-slot {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 2px solid #3d2919;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.achievement-slot:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.achievement-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
filter: drop-shadow(2px 2px 0 #000);
|
||||||
|
}
|
||||||
|
.locked { filter: grayscale(1) brightness(0.5); }
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 120%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #111;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.achievement-slot:hover .tooltip { opacity: 1; }
|
||||||
|
|
||||||
|
/* Learning Path */
|
||||||
|
.path-row { margin-bottom: 15px; }
|
||||||
|
.path-label { font-size: 12px; margin-bottom: 5px; display: flex; justify-content: space-between; }
|
||||||
|
.path-track {
|
||||||
|
height: 16px;
|
||||||
|
background: #000;
|
||||||
|
border: 2px solid #555;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.path-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--diamond);
|
||||||
|
width: 0;
|
||||||
|
transition: width 1s ease-out;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.path-fill::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; right: 0; bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
background: #fff;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leaderboard */
|
||||||
|
.leaderboard-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 2px solid #333;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.leaderboard-item.highlight {
|
||||||
|
background: rgba(255, 215, 0, 0.2);
|
||||||
|
border: 2px solid var(--gold);
|
||||||
|
}
|
||||||
|
.rank { width: 30px; text-align: center; font-weight: bold; }
|
||||||
|
.user { flex-grow: 1; padding-left: 10px; }
|
||||||
|
.score { color: var(--gold); }
|
||||||
|
|
||||||
|
/* Daily Tasks */
|
||||||
|
.task-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px dashed #444;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.checkbox-custom {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #777;
|
||||||
|
background: #000;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.checked .checkbox-custom::after {
|
||||||
|
content: '✓';
|
||||||
|
color: var(--minecraft-green);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.task-xp { margin-left: auto; color: #888; font-size: 10px; }
|
||||||
|
|
||||||
|
/* Treasure Chest */
|
||||||
|
.treasure-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.chest-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: var(--gold);
|
||||||
|
filter: drop-shadow(0 0 10px var(--gold));
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
.chest-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100px; height: 100px;
|
||||||
|
background: radial-gradient(circle, rgba(255,215,0,0.4) 0%, transparent 70%);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
||||||
|
50% { transform: translate(-50%, -50%) scale(1.5); opacity: 0.8; }
|
||||||
|
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; w-full; h-full;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
z-index: 1000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: #c6c6c6;
|
||||||
|
border: 4px solid #fff;
|
||||||
|
padding: 4px;
|
||||||
|
width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.modal-inner {
|
||||||
|
background: #222;
|
||||||
|
border: 4px solid #555;
|
||||||
|
padding: 30px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.reward-icon { font-size: 48px; margin: 20px 0; display: block; }
|
||||||
|
.close-btn {
|
||||||
|
background: var(--red);
|
||||||
|
border: 2px solid #fff;
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="flex justify-between items-center">
|
||||||
|
<div class="nav-brand"><i class="ri-code-box-line"></i> CRAFTCODE</div>
|
||||||
|
<div class="nav-links flex">
|
||||||
|
<a href="#" class="mc-btn">Home</a>
|
||||||
|
<a href="#" class="mc-btn">Problems</a>
|
||||||
|
<a href="#" class="mc-btn active">Profile</a>
|
||||||
|
<a href="#" class="mc-btn">Shop</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- User Hero Section -->
|
||||||
|
<div class="mc-wood-panel user-hero">
|
||||||
|
<div class="avatar-container">
|
||||||
|
<!-- Using a pixelated avatar placeholder -->
|
||||||
|
<img src="https://api.dicebear.com/7.x/pixel-art/svg?seed=Steve&backgroundColor=b6e3f4" alt="Avatar" class="avatar-img">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-details flex flex-col justify-center">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1>CodeMaster2024</h1>
|
||||||
|
</div>
|
||||||
|
<div style="color: #ddd; font-size: 14px; margin-bottom: 10px;">Passionate problem solver</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="badge-lv"><i class="ri-shield-star-fill"></i> Lv.25</div>
|
||||||
|
<div class="title-banner">Algorithm Expert</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="xp-bar-container">
|
||||||
|
<div class="xp-bar-fill" id="mainXP" data-width="84.5%"></div>
|
||||||
|
<div class="xp-text">8,450 / 10,000 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">128</div>
|
||||||
|
<div class="stat-label">Problems</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">456</div>
|
||||||
|
<div class="stat-label">Submits</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">85%</div>
|
||||||
|
<div class="stat-label">Acceptance</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">15🔥</div>
|
||||||
|
<div class="stat-label">Streak</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Three Column Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div class="left-col">
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Difficulty Stats</div>
|
||||||
|
<div class="pie-chart-wrap"></div>
|
||||||
|
<div class="flex justify-center gap-4" style="font-size: 10px; margin-top: 10px;">
|
||||||
|
<span style="color:var(--minecraft-green)">Easy</span>
|
||||||
|
<span style="color:var(--gold)">Med</span>
|
||||||
|
<span style="color:var(--red)">Hard</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Activity Log</div>
|
||||||
|
<div style="font-size: 10px; color: #aaa; text-align: center; margin-bottom: 5px;">Last 30 Days</div>
|
||||||
|
<div class="activity-grid" id="activityGrid">
|
||||||
|
<!-- Generated by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Timeline</div>
|
||||||
|
<div class="flex flex-col gap-2" style="font-size: 10px; color: #ccc;">
|
||||||
|
<div style="border-left: 2px solid #555; padding-left: 8px;">
|
||||||
|
<div style="color: var(--minecraft-green)">Solved "Two Sum"</div>
|
||||||
|
<div style="font-size: 8px; color: #888;">2 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-left: 2px solid #555; padding-left: 8px;">
|
||||||
|
<div style="color: var(--red)">Failed "Dijkstra"</div>
|
||||||
|
<div style="font-size: 8px; color: #888;">5 hours ago</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-left: 2px solid #555; padding-left: 8px;">
|
||||||
|
<div style="color: var(--gold)">Badge Unlocked</div>
|
||||||
|
<div style="font-size: 8px; color: #888;">1 day ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center Column -->
|
||||||
|
<div class="center-col">
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Achievement Wall</div>
|
||||||
|
<div class="achievement-grid">
|
||||||
|
<!-- 15 Slots: 5x3 -->
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-sword-fill achievement-icon" style="color: #cd7f32;"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Novice Slayer</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Solve 10 Easy</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+50 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-sword-fill achievement-icon" style="color: #eee;"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Iron Will</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Solve 50 Medium</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+200 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-sword-fill achievement-icon" style="color: var(--diamond);"></i>
|
||||||
|
<div class="tooltip">LOCKED: Diamond Blade</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-trophy-fill achievement-icon" style="color: var(--gold);"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Champion</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Win a Contest</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+1000 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-book-3-fill achievement-icon" style="color: #a0522d;"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Scholar</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Read 100 Articles</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+100 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-flask-fill achievement-icon" style="color: #ff69b4;"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Alchemist</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Optimize 10 times</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+150 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-fire-fill achievement-icon" style="color: orange;"></i>
|
||||||
|
<div class="tooltip">LOCKED: Inferno</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot">
|
||||||
|
<i class="ri-flashlight-fill achievement-icon" style="color: yellow;"></i>
|
||||||
|
<div class="tooltip">
|
||||||
|
<div style="color: var(--gold)">Speedster</div>
|
||||||
|
<div style="margin-top:4px; color:#aaa">Solve in 5 mins</div>
|
||||||
|
<div style="color: var(--minecraft-green)">+50 XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-star-fill achievement-icon" style="color: cyan;"></i>
|
||||||
|
<div class="tooltip">LOCKED: Star Player</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-map-pin-user-fill achievement-icon" style="color: var(--minecraft-green);"></i>
|
||||||
|
<div class="tooltip">LOCKED: Explorer</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-shield-fill achievement-icon" style="color: gray;"></i>
|
||||||
|
<div class="tooltip">LOCKED: Guardian</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-hammer-fill achievement-icon" style="color: #ddd;"></i>
|
||||||
|
<div class="tooltip">LOCKED: Builder</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-bug-fill achievement-icon" style="color: var(--red);"></i>
|
||||||
|
<div class="tooltip">LOCKED: Bug Hunter</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-compass-3-fill achievement-icon" style="color: gold;"></i>
|
||||||
|
<div class="tooltip">LOCKED: Navigator</div>
|
||||||
|
</div>
|
||||||
|
<div class="achievement-slot locked">
|
||||||
|
<i class="ri-vip-crown-fill achievement-icon" style="color: var(--purple);"></i>
|
||||||
|
<div class="tooltip">LOCKED: King</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Learning Path</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<div class="path-label">
|
||||||
|
<span>Basics</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-track"><div class="path-fill" data-width="100%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<div class="path-label">
|
||||||
|
<span>Algorithms</span>
|
||||||
|
<span>75%</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-track"><div class="path-fill" data-width="75%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<div class="path-label">
|
||||||
|
<span>Data Structures</span>
|
||||||
|
<span>60%</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-track"><div class="path-fill" data-width="60%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<div class="path-label">
|
||||||
|
<span>Advanced DP</span>
|
||||||
|
<span>40%</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-track"><div class="path-fill" data-width="40%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="path-row">
|
||||||
|
<div class="path-label">
|
||||||
|
<span>Contest Prep</span>
|
||||||
|
<span>20%</span>
|
||||||
|
</div>
|
||||||
|
<div class="path-track"><div class="path-fill" data-width="20%"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="right-col">
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Leaderboard</div>
|
||||||
|
<div class="leaderboard-item">
|
||||||
|
<div class="rank">1</div>
|
||||||
|
<div class="user">Notch <i class="ri-vip-crown-fill" style="color:gold"></i></div>
|
||||||
|
<div class="score">99k</div>
|
||||||
|
</div>
|
||||||
|
<div class="leaderboard-item">
|
||||||
|
<div class="rank">2</div>
|
||||||
|
<div class="user">Jeb_ <i class="ri-medal-fill" style="color:silver"></i></div>
|
||||||
|
<div class="score">85k</div>
|
||||||
|
</div>
|
||||||
|
<div class="leaderboard-item">
|
||||||
|
<div class="rank">3</div>
|
||||||
|
<div class="user">Alex <i class="ri-medal-fill" style="color:#cd7f32"></i></div>
|
||||||
|
<div class="score">72k</div>
|
||||||
|
</div>
|
||||||
|
<div class="leaderboard-item">
|
||||||
|
<div class="rank">4</div>
|
||||||
|
<div class="user">Herobrine</div>
|
||||||
|
<div class="score">66k</div>
|
||||||
|
</div>
|
||||||
|
<div class="leaderboard-item highlight">
|
||||||
|
<div class="rank">5</div>
|
||||||
|
<div class="user">CodeMaster</div>
|
||||||
|
<div class="score">50k</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner">
|
||||||
|
<div class="card-header">Daily Quests</div>
|
||||||
|
<div class="task-item checked">
|
||||||
|
<div class="checkbox-custom"></div>
|
||||||
|
<div>Login</div>
|
||||||
|
<div class="task-xp">10XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-item checked">
|
||||||
|
<div class="checkbox-custom"></div>
|
||||||
|
<div>Solve 1 Easy</div>
|
||||||
|
<div class="task-xp">20XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-item checked">
|
||||||
|
<div class="checkbox-custom"></div>
|
||||||
|
<div>Review Code</div>
|
||||||
|
<div class="task-xp">15XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<div class="checkbox-custom"></div>
|
||||||
|
<div>Solve 1 Hard</div>
|
||||||
|
<div class="task-xp">100XP</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<div class="checkbox-custom"></div>
|
||||||
|
<div>Forum Post</div>
|
||||||
|
<div class="task-xp">30XP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-card">
|
||||||
|
<div class="col-card-inner" style="display:flex; justify-content:center; align-items:center;">
|
||||||
|
<div class="treasure-box" id="chestBtn">
|
||||||
|
<div class="chest-glow"></div>
|
||||||
|
<i class="ri-treasure-map-line chest-icon"></i>
|
||||||
|
<div style="margin-top:10px; font-size: 10px; color: var(--gold);">Daily Reward</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reward Modal -->
|
||||||
|
<div class="modal" id="rewardModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-inner">
|
||||||
|
<h2>CHEST OPENED!</h2>
|
||||||
|
<i class="ri-diamond-fill reward-icon" style="color: var(--diamond);"></i>
|
||||||
|
<p>You found <span style="color:var(--gold); font-size: 20px;">500 XP</span></p>
|
||||||
|
<p style="font-size: 10px; color: #aaa; margin-top: 10px;">Rare Item Found: Diamond Boots</p>
|
||||||
|
<button class="close-btn" id="closeModal">CLAIM</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Animate XP Bar
|
||||||
|
const xpBar = document.getElementById('mainXP');
|
||||||
|
setTimeout(() => {
|
||||||
|
xpBar.style.width = xpBar.getAttribute('data-width');
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Animate Learning Paths
|
||||||
|
const paths = document.querySelectorAll('.path-fill');
|
||||||
|
paths.forEach(path => {
|
||||||
|
setTimeout(() => {
|
||||||
|
path.style.width = path.getAttribute('data-width');
|
||||||
|
}, 800);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activity Grid Generator
|
||||||
|
const activityGrid = document.getElementById('activityGrid');
|
||||||
|
for(let i=0; i<30; i++) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.className = 'activity-cell';
|
||||||
|
// Random activity level 0-4
|
||||||
|
const level = Math.random() > 0.3 ? Math.floor(Math.random() * 5) : 0;
|
||||||
|
if(level > 0) cell.setAttribute('data-level', level);
|
||||||
|
|
||||||
|
// Add tooltip on hover
|
||||||
|
cell.title = `Day ${30-i}: ${level*2} submissions`;
|
||||||
|
activityGrid.appendChild(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task Checkbox Logic
|
||||||
|
const tasks = document.querySelectorAll('.task-item');
|
||||||
|
tasks.forEach(task => {
|
||||||
|
task.addEventListener('click', () => {
|
||||||
|
task.classList.toggle('checked');
|
||||||
|
// Add simple sound effect logic here if audio was allowed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Treasure Chest Logic
|
||||||
|
const chestBtn = document.getElementById('chestBtn');
|
||||||
|
const modal = document.getElementById('rewardModal');
|
||||||
|
const closeBtn = document.getElementById('closeModal');
|
||||||
|
|
||||||
|
chestBtn.addEventListener('click', () => {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
});
|
||||||
|
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal on outside click
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if(e.target === modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
在新工单中引用
屏蔽一个用户