9 Commits

Author SHA1 Message Date
Torsten Schulz (local)
4ac71d967f Fügt Unterstützung für myTischtennis-Integration hinzu. Aktualisiert die Mitglieder-Controller und -Routen, um die Aktualisierung von TTR/QTTR-Werten zu ermöglichen. Ergänzt die Benutzeroberfläche in MembersView.vue zur Aktualisierung der Bewertungen und fügt neue Routen für die myTischtennis-Daten hinzu. Aktualisiert die Datenmodelle, um die neuen Felder für TTR und QTTR zu integrieren. 2025-10-01 12:09:55 +02:00
Torsten Schulz (local)
75d304ec6d Merge branch 'activitypainter' 2025-10-01 09:42:33 +02:00
Torsten Schulz (local)
afd96f5df1 Optimiert die Berechnung der Startposition im CourtDrawingRender.vue, indem die Offset-Logik für vertikale und horizontale Zeichnungen präzisiert wird. Kommentiert den Code zur besseren Verständlichkeit der Offset-Berechnungen. 2025-10-01 09:42:15 +02:00
Torsten Schulz (local)
4bfa6a5889 Erweitert die Funktionalität in CourtDrawingRender.vue und CourtDrawingTool.vue zur Verbesserung der Zeichnungslogik. Fügt neue Offset-Parameter für Zielkreise hinzu und optimiert die Berechnung der Zielpositionen. Entfernt die Schaltflächen für das manuelle Speichern in CourtDrawingTool.vue zugunsten einer automatischen Speicherung. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue zur Unterstützung der neuen Zeichnungsdaten-Logik. 2025-10-01 09:41:07 +02:00
Torsten Schulz (local)
f4187512ba Erweitert die Funktionalität zur Erstellung und Aktualisierung von vordefinierten Aktivitäten, indem das Feld für Zeichnungsdaten in den entsprechenden Controllern, Modellen und Services hinzugefügt wird. Aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue und PredefinedActivities.vue, um die Handhabung von Zeichnungsdaten zu verbessern und die Logik für das Laden und Speichern von Zeichnungen zu optimieren. 2025-09-25 19:35:13 +02:00
Torsten Schulz (local)
b557297bf0 Verbessert die Logik zur Erstellung von Aktivitäten im DiaryDateActivityService, um PredefinedActivities robuster zu finden. Fügt Unterstützung für die Suche nach Aktivitäten per ID, Name oder Code hinzu. Aktualisiert die Benutzeroberfläche in DiaryView.vue zur Anzeige von Zeichnungsdaten und integriert ein neues Rendering-Modal für Zeichnungen. Optimiert die Bildanzeige in CourtDrawingTool.vue und implementiert eine verbesserte Fehlerbehandlung beim Laden von Bildern. 2025-09-23 14:40:41 +02:00
Torsten Schulz (local)
eb2273e28c Aktualisiert die Token-Lebensdauer im Authentifizierungsdienst auf 3 Stunden und verbessert die Logik zur Auswahl der Startposition im CourtDrawingTool.vue, um eine Standard-Startposition festzulegen, wenn keine ausgewählt ist. 2025-09-23 09:13:51 +02:00
Torsten Schulz (local)
091599b745 Erweitert die Funktionalität in PredefinedActivityImageController.js, um Zeichnungsdaten aus dem Request zu extrahieren und in der Datenbank zu speichern. Aktualisiert das Datenmodell in PredefinedActivityImage.js, um ein neues Feld für Zeichnungsdaten hinzuzufügen. Passt die Routen in predefinedActivityRoutes.js an, um die neue PUT-Methode für das Hochladen von Bildern zu unterstützen. Integriert die Zeichnungsdaten in die Aktivitätenlogik in diaryDateActivityService.js und aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue zur Unterstützung von Zeichnungsdaten. Verbessert die Handhabung von Bild-Uploads in PredefinedActivities.vue und implementiert die Logik zum Laden von Zeichnungsdaten beim Bearbeiten von Aktivitäten. 2025-09-23 08:39:13 +02:00
Torsten Schulz (local)
d70a5ca63e Erweitert die Funktionalität in PredefinedActivities.vue um die Möglichkeit, eine Übungszeichnung zu erstellen. Fügt ein Zeichen-Tool hinzu, das die Zeichnungsdaten speichert und automatisch als Bild-Link verwendet, wenn kein Bild-Link vorhanden ist. Aktualisiert die Benutzeroberfläche zur Bild- und Zeichnungshinzufügung. 2025-09-22 12:23:39 +02:00
64 changed files with 4993 additions and 513 deletions

View File

@@ -0,0 +1,284 @@
import axios from 'axios';
const BASE_URL = 'https://www.mytischtennis.de';
class MyTischtennisClient {
constructor() {
this.baseURL = BASE_URL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*'
},
maxRedirects: 0, // Don't follow redirects automatically
validateStatus: (status) => status >= 200 && status < 400 // Accept 3xx as success
});
}
/**
* Login to myTischtennis API
* @param {string} email - myTischtennis email (not username!)
* @param {string} password - myTischtennis password
* @returns {Promise<Object>} Login response with token and session data
*/
async login(email, password) {
try {
// Create form data
const formData = new URLSearchParams();
formData.append('email', email);
formData.append('password', password);
formData.append('intent', 'login');
const response = await this.client.post(
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
formData.toString()
);
// Extract the cookie from response headers
const setCookie = response.headers['set-cookie'];
if (!setCookie || !Array.isArray(setCookie)) {
return {
success: false,
error: 'Keine Session-Cookie erhalten'
};
}
// Find the sb-10-auth-token cookie
const authCookie = setCookie.find(cookie => cookie.startsWith('sb-10-auth-token='));
if (!authCookie) {
return {
success: false,
error: 'Kein Auth-Token in Response gefunden'
};
}
// Extract and decode the token
const tokenMatch = authCookie.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
return {
success: false,
error: 'Token-Format ungültig'
};
}
const base64Token = tokenMatch[1];
let tokenData;
try {
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
tokenData = JSON.parse(decodedToken);
} catch (decodeError) {
console.error('Error decoding token:', decodeError);
return {
success: false,
error: 'Token konnte nicht dekodiert werden'
};
}
return {
success: true,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt: tokenData.expires_at,
expiresIn: tokenData.expires_in,
user: tokenData.user,
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
};
} catch (error) {
console.error('MyTischtennis login error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Verify login credentials
* @param {string} email - myTischtennis email
* @param {string} password - myTischtennis password
* @returns {Promise<boolean>} True if credentials are valid
*/
async verifyCredentials(email, password) {
const result = await this.login(email, password);
return result.success;
}
/**
* Make an authenticated request
* @param {string} endpoint - API endpoint
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @param {Object} options - Additional axios options
* @returns {Promise<Object>} API response
*/
async authenticatedRequest(endpoint, cookie, options = {}) {
try {
const response = await this.client.request({
url: endpoint,
...options,
headers: {
...options.headers,
'Cookie': cookie,
'Accept': '*/*',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://www.mytischtennis.de/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin'
}
});
return {
success: true,
data: response.data
};
} catch (error) {
console.error('MyTischtennis API error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Get user profile and club information
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
console.log('[getUserProfile] - Calling /?_data=root with cookie:', cookie?.substring(0, 50) + '...');
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
console.log('[getUserProfile] - Result success:', result.success);
if (result.success) {
console.log('[getUserProfile] - Response structure:', {
hasUserProfile: !!result.data?.userProfile,
hasClub: !!result.data?.userProfile?.club,
hasOrganization: !!result.data?.userProfile?.organization,
clubnr: result.data?.userProfile?.club?.clubnr,
clubName: result.data?.userProfile?.club?.name,
orgShort: result.data?.userProfile?.organization?.short,
ttr: result.data?.userProfile?.ttr,
qttr: result.data?.userProfile?.qttr
});
console.log('[getUserProfile] - Full userProfile.club:', result.data?.userProfile?.club);
console.log('[getUserProfile] - Full userProfile.organization:', result.data?.userProfile?.organization);
return {
success: true,
clubId: result.data?.userProfile?.club?.clubnr || null,
clubName: result.data?.userProfile?.club?.name || null,
fedNickname: result.data?.userProfile?.organization?.short || null,
ttr: result.data?.userProfile?.ttr || null,
qttr: result.data?.userProfile?.qttr || null,
userProfile: result.data?.userProfile || null
};
}
console.error('[getUserProfile] - Failed:', result.error);
return result;
}
/**
* Get club rankings (andro-Rangliste)
* @param {string} cookie - Authentication cookie
* @param {string} clubId - Club number (e.g., "43030")
* @param {string} fedNickname - Federation nickname (e.g., "HeTTV")
* @returns {Promise<Object>} Rankings with player entries (all pages)
*/
async getClubRankings(cookie, clubId, fedNickname) {
const allEntries = [];
let currentPage = 0;
let hasMorePages = true;
console.log('[getClubRankings] - Starting to fetch rankings for club', clubId);
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
console.log(`[getClubRankings] - Fetching page ${currentPage}...`);
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
});
if (!result.success) {
console.error(`[getClubRankings] - Failed to fetch page ${currentPage}:`, result.error);
return result;
}
// Find the dynamic key that contains entries
const blockLoaderData = result.data?.pageContent?.blockLoaderData;
if (!blockLoaderData) {
console.error('[getClubRankings] - No blockLoaderData found');
return {
success: false,
error: 'Keine blockLoaderData gefunden'
};
}
// Finde den Schlüssel, der entries enthält
let entries = null;
let rankingData = null;
for (const key in blockLoaderData) {
if (blockLoaderData[key]?.entries) {
entries = blockLoaderData[key].entries;
rankingData = blockLoaderData[key];
break;
}
}
if (!entries) {
console.error('[getClubRankings] - No entries found in blockLoaderData');
return {
success: false,
error: 'Keine entries in blockLoaderData gefunden'
};
}
console.log(`[getClubRankings] - Page ${currentPage}: Found ${entries.length} entries`);
// Füge Entries hinzu
allEntries.push(...entries);
// Prüfe ob es weitere Seiten gibt
// Wenn die aktuelle Seite weniger Einträge hat als das Limit, sind wir am Ende
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
console.log('[getClubRankings] - No more entries, stopping');
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
console.log(`[getClubRankings] - Reached last page (${rankingData.numberOfPages})`);
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
console.log(`[getClubRankings] - Got all entries (${allEntries.length}/${rankingData.resultLength})`);
} else {
currentPage++;
}
}
console.log(`[getClubRankings] - Total entries fetched: ${allEntries.length}`);
return {
success: true,
entries: allEntries,
metadata: {
totalEntries: allEntries.length,
pagesFetched: currentPage + 1
}
};
}
}
export default new MyTischtennisClient();

View File

@@ -34,11 +34,11 @@ const getWaitingApprovals = async(req, res) => {
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender } = req.body;
testMembership, picsInInternetAllowed, gender, ttr, qttr } = req.body;
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender);
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr);
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
@@ -75,4 +75,17 @@ const getMemberImage = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };
const updateRatingsFromMyTischtennis = async (req, res) => {
console.log('[updateRatingsFromMyTischtennis]');
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.updateRatingsFromMyTischtennis(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
res.status(500).json({ error: 'Failed to update ratings' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };

View File

@@ -0,0 +1,133 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisController {
/**
* GET /api/mytischtennis/account
* Get current user's myTischtennis account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const account = await myTischtennisService.getAccount(userId);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const status = await myTischtennisService.checkAccountStatus(userId);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update myTischtennis account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
}
const account = await myTischtennisService.upsertAccount(
userId,
email,
password,
savePassword || false,
userPassword
);
res.status(200).json({
message: 'myTischtennis-Account erfolgreich gespeichert',
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account
* Delete myTischtennis account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password } = req.body;
const result = await myTischtennisService.verifyLogin(userId, password);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const session = await myTischtennisService.getSession(userId);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -5,8 +5,8 @@ import fs from 'fs';
export const createPredefinedActivity = async (req, res) => {
try {
const { name, code, description, durationText, duration, imageLink } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData });
res.status(201).json(predefinedActivity);
} catch (error) {
console.error('[createPredefinedActivity] - Error:', error);
@@ -42,8 +42,8 @@ export const getPredefinedActivityById = async (req, res) => {
export const updatePredefinedActivity = async (req, res) => {
try {
const { id } = req.params;
const { name, code, description, durationText, duration, imageLink } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData });
res.status(200).json(updatedActivity);
} catch (error) {
console.error('[updatePredefinedActivity] - Error:', error);

View File

@@ -33,10 +33,15 @@ export const uploadPredefinedActivityImage = async (req, res) => {
.jpeg({ quality: 85 })
.toFile(filePath);
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
console.log('[uploadPredefinedActivityImage] - drawingData:', drawingData);
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,
imagePath: filePath,
mimeType: 'image/jpeg',
drawingData: drawingData ? JSON.stringify(drawingData) : null,
});
// Optional: als imageLink am Activity-Datensatz setzen

View File

@@ -0,0 +1,11 @@
-- Migration: Add drawing_data column to predefined_activity_images table
-- Date: 2025-09-22
-- Description: Adds drawing_data column to store Court Drawing Tool metadata
ALTER TABLE `predefined_activity_images`
ADD COLUMN `drawing_data` TEXT NULL
COMMENT 'JSON string containing drawing metadata for Court Drawing Tool'
AFTER `mime_type`;
-- Verify the column was added
DESCRIBE `predefined_activity_images`;

View File

@@ -127,6 +127,16 @@ const Member = sequelize.define('Member', {
type: DataTypes.ENUM('male','female','diverse','unknown'),
allowNull: true,
defaultValue: 'unknown'
},
ttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
},
qttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
}
}, {
underscored: true,

View File

@@ -0,0 +1,122 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const MyTischtennis = sequelize.define('MyTischtennis', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
encryptedPassword: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_password'
},
savePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'save_password'
},
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'access_token'
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'refresh_token'
},
expiresAt: {
type: DataTypes.BIGINT,
allowNull: true,
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
},
userData: {
type: DataTypes.JSON,
allowNull: true,
field: 'user_data'
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_id'
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_name'
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
field: 'fed_nickname'
},
lastLoginAttempt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_attempt'
},
lastLoginSuccess: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_success'
}
}, {
underscored: true,
tableName: 'my_tischtennis',
timestamps: true,
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
MyTischtennis.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
MyTischtennis.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting myTischtennis password:', error);
return null;
}
}
return null;
};
export default MyTischtennis;

View File

@@ -19,6 +19,11 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
type: DataTypes.TEXT,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string with metadata for Court Drawing Tool'
},
durationText: {
type: DataTypes.STRING,
allowNull: true,

View File

@@ -19,6 +19,11 @@ const PredefinedActivityImage = sequelize.define('PredefinedActivityImage', {
type: DataTypes.STRING,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string containing drawing metadata for Court Drawing Tool'
},
}, {
tableName: 'predefined_activity_images',
timestamps: true,

View File

@@ -33,6 +33,7 @@ import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
import OfficialCompetition from './OfficialCompetition.js';
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
import MyTischtennis from './MyTischtennis.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -204,6 +205,9 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
export {
User,
Log,
@@ -239,4 +243,5 @@ export {
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
MyTischtennis,
};

View File

@@ -4,6 +4,15 @@
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -810,6 +819,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -818,6 +833,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -955,6 +981,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1084,6 +1123,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1227,6 +1278,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1259,6 +1326,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1315,6 +1391,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1342,13 +1432,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1362,6 +1449,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1752,6 +1866,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1849,16 +1999,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1867,6 +2022,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1913,12 +2081,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1945,10 +2113,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1957,11 +2125,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2405,6 +2576,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2972,6 +3152,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.1](https://github.com/ljharb/es-define-property/compare/v1.0.0...v1.0.1) - 2024-12-06
### Commits
- [types] use shared tsconfig [`954a663`](https://github.com/ljharb/es-define-property/commit/954a66360326e508a0e5daa4b07493d58f5e110e)
- [actions] split out node 10-20, and 20+ [`3a8e84b`](https://github.com/ljharb/es-define-property/commit/3a8e84b23883f26ff37b3e82ff283834228e18c6)
- [Dev Deps] update `@ljharb/eslint-config`, `@ljharb/tsconfig`, `@types/get-intrinsic`, `@types/tape`, `auto-changelog`, `gopd`, `tape` [`86ae27b`](https://github.com/ljharb/es-define-property/commit/86ae27bb8cc857b23885136fad9cbe965ae36612)
- [Refactor] avoid using `get-intrinsic` [`02480c0`](https://github.com/ljharb/es-define-property/commit/02480c0353ef6118965282977c3864aff53d98b1)
- [Tests] replace `aud` with `npm audit` [`f6093ff`](https://github.com/ljharb/es-define-property/commit/f6093ff74ab51c98015c2592cd393bd42478e773)
- [Tests] configure testling [`7139e66`](https://github.com/ljharb/es-define-property/commit/7139e66959247a56086d9977359caef27c6849e7)
- [Dev Deps] update `tape` [`b901b51`](https://github.com/ljharb/es-define-property/commit/b901b511a75e001a40ce1a59fef7d9ffcfc87482)
- [Tests] fix types in tests [`469d269`](https://github.com/ljharb/es-define-property/commit/469d269fd141b1e773ec053a9fa35843493583e0)
- [Dev Deps] add missing peer dep [`733acfb`](https://github.com/ljharb/es-define-property/commit/733acfb0c4c96edf337e470b89a25a5b3724c352)
## v1.0.0 - 2024-02-12
### Commits

View File

@@ -1,9 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
/** @type {import('.')} */
var $defineProperty = GetIntrinsic('%Object.defineProperty%', true) || false;
var $defineProperty = Object.defineProperty || false;
if ($defineProperty) {
try {
$defineProperty({}, 'a', { value: 1 });

View File

@@ -1,6 +1,6 @@
{
"name": "es-define-property",
"version": "1.0.0",
"version": "1.0.1",
"description": "`Object.defineProperty`, but not IE 8's broken one.",
"main": "index.js",
"types": "./index.d.ts",
@@ -19,7 +19,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -42,29 +42,29 @@
"url": "https://github.com/ljharb/es-define-property/issues"
},
"homepage": "https://github.com/ljharb/es-define-property#readme",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/get-intrinsic": "^1.2.2",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.2",
"@types/gopd": "^1.0.3",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "^8.8.0",
"evalmd": "^0.0.19",
"gopd": "^1.0.1",
"gopd": "^1.2.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4",
"tape": "^5.9.0",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"testling": {
"files": "test/index.js"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",

View File

@@ -10,6 +10,7 @@ test('defineProperty: supported', { skip: !$defineProperty }, function (t) {
t.equal(typeof $defineProperty, 'function', 'defineProperty is supported');
if ($defineProperty && gOPD) { // this `if` check is just to shut TS up
/** @type {{ a: number, b?: number, c?: number }} */
var o = { a: 1 };
$defineProperty(o, 'b', { enumerable: true, value: 2 });

View File

@@ -1,47 +1,7 @@
{
"extends": "@ljharb/tsconfig",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
/* Language and Environment */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": ["types"], /* Specify multiple folders that act like `./node_modules/@types`. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow `import x from y` when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
"target": "es2022",
},
"exclude": [
"coverage",

View File

@@ -11,6 +11,10 @@
"es2022": true,
},
"globals": {
"Float16Array": false,
},
"rules": {
"array-bracket-newline": 0,
"complexity": 0,

View File

@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.3.0](https://github.com/ljharb/get-intrinsic/compare/v1.2.7...v1.3.0) - 2025-02-22
### Commits
- [Dev Deps] update `es-abstract`, `es-value-fixtures`, `for-each`, `object-inspect` [`9b61553`](https://github.com/ljharb/get-intrinsic/commit/9b61553c587f1c1edbd435597e88c7d387da97dd)
- [Deps] update `call-bind-apply-helpers`, `es-object-atoms`, `get-proto` [`a341fee`](https://github.com/ljharb/get-intrinsic/commit/a341fee0f39a403b0f0069e82c97642d5eb11043)
- [New] add `Float16Array` [`de22116`](https://github.com/ljharb/get-intrinsic/commit/de22116b492fb989a0341bceb6e573abfaed73dc)
## [v1.2.7](https://github.com/ljharb/get-intrinsic/compare/v1.2.6...v1.2.7) - 2025-01-02
### Commits
- [Refactor] use `get-proto` directly [`00ab955`](https://github.com/ljharb/get-intrinsic/commit/00ab95546a0980c8ad42a84253daaa8d2adcedf9)
- [Deps] update `math-intrinsics` [`c716cdd`](https://github.com/ljharb/get-intrinsic/commit/c716cdd6bbe36b438057025561b8bb5a879ac8a0)
- [Dev Deps] update `call-bound`, `es-abstract` [`dc648a6`](https://github.com/ljharb/get-intrinsic/commit/dc648a67eb359037dff8d8619bfa71d86debccb1)
## [v1.2.6](https://github.com/ljharb/get-intrinsic/compare/v1.2.5...v1.2.6) - 2024-12-11
### Commits
- [Refactor] use `math-intrinsics` [`841be86`](https://github.com/ljharb/get-intrinsic/commit/841be8641a9254c4c75483b30c8871b5d5065926)
- [Refactor] use `es-object-atoms` [`42057df`](https://github.com/ljharb/get-intrinsic/commit/42057dfa16f66f64787e66482af381cc6f31d2c1)
- [Deps] update `call-bind-apply-helpers` [`45afa24`](https://github.com/ljharb/get-intrinsic/commit/45afa24a9ee4d6d3c172db1f555b16cb27843ef4)
- [Dev Deps] update `call-bound` [`9cba9c6`](https://github.com/ljharb/get-intrinsic/commit/9cba9c6e70212bc163b7a5529cb25df46071646f)
## [v1.2.5](https://github.com/ljharb/get-intrinsic/compare/v1.2.4...v1.2.5) - 2024-12-06
### Commits
- [actions] split out node 10-20, and 20+ [`6e2b9dd`](https://github.com/ljharb/get-intrinsic/commit/6e2b9dd23902665681ebe453256ccfe21d7966f0)
- [Refactor] use `dunder-proto` and `call-bind-apply-helpers` instead of `has-proto` [`c095d17`](https://github.com/ljharb/get-intrinsic/commit/c095d179ad0f4fbfff20c8a3e0cb4fe668018998)
- [Refactor] use `gopd` [`9841d5b`](https://github.com/ljharb/get-intrinsic/commit/9841d5b35f7ab4fd2d193f0c741a50a077920e90)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `es-abstract`, `es-value-fixtures`, `gopd`, `mock-property`, `object-inspect`, `tape` [`2d07e01`](https://github.com/ljharb/get-intrinsic/commit/2d07e01310cee2cbaedfead6903df128b1f5d425)
- [Deps] update `gopd`, `has-proto`, `has-symbols`, `hasown` [`974d8bf`](https://github.com/ljharb/get-intrinsic/commit/974d8bf5baad7939eef35c25cc1dd88c10a30fa6)
- [Dev Deps] update `call-bind`, `es-abstract`, `tape` [`df9dde1`](https://github.com/ljharb/get-intrinsic/commit/df9dde178186631ab8a3165ede056549918ce4bc)
- [Refactor] cache `es-define-property` as well [`43ef543`](https://github.com/ljharb/get-intrinsic/commit/43ef543cb02194401420e3a914a4ca9168691926)
- [Deps] update `has-proto`, `has-symbols`, `hasown` [`ad4949d`](https://github.com/ljharb/get-intrinsic/commit/ad4949d5467316505aad89bf75f9417ed782f7af)
- [Tests] use `call-bound` directly [`ad5c406`](https://github.com/ljharb/get-intrinsic/commit/ad5c4069774bfe90e520a35eead5fe5ca9d69e80)
- [Deps] update `has-proto`, `hasown` [`45414ca`](https://github.com/ljharb/get-intrinsic/commit/45414caa312333a2798953682c68f85c550627dd)
- [Tests] replace `aud` with `npm audit` [`18d3509`](https://github.com/ljharb/get-intrinsic/commit/18d3509f79460e7924da70409ee81e5053087523)
- [Deps] update `es-define-property` [`aadaa3b`](https://github.com/ljharb/get-intrinsic/commit/aadaa3b2188d77ad9bff394ce5d4249c49eb21f5)
- [Dev Deps] add missing peer dep [`c296a16`](https://github.com/ljharb/get-intrinsic/commit/c296a16246d0c9a5981944f4cc5cf61fbda0cf6a)
## [v1.2.4](https://github.com/ljharb/get-intrinsic/compare/v1.2.3...v1.2.4) - 2024-02-05
### Commits

View File

@@ -2,6 +2,8 @@
var undefined;
var $Object = require('es-object-atoms');
var $Error = require('es-errors');
var $EvalError = require('es-errors/eval');
var $RangeError = require('es-errors/range');
@@ -10,6 +12,14 @@ var $SyntaxError = require('es-errors/syntax');
var $TypeError = require('es-errors/type');
var $URIError = require('es-errors/uri');
var abs = require('math-intrinsics/abs');
var floor = require('math-intrinsics/floor');
var max = require('math-intrinsics/max');
var min = require('math-intrinsics/min');
var pow = require('math-intrinsics/pow');
var round = require('math-intrinsics/round');
var sign = require('math-intrinsics/sign');
var $Function = Function;
// eslint-disable-next-line consistent-return
@@ -19,14 +29,8 @@ var getEvalledConstructor = function (expressionSyntax) {
} catch (e) {}
};
var $gOPD = Object.getOwnPropertyDescriptor;
if ($gOPD) {
try {
$gOPD({}, '');
} catch (e) {
$gOPD = null; // this is IE 8, which has a broken gOPD
}
}
var $gOPD = require('gopd');
var $defineProperty = require('es-define-property');
var throwTypeError = function () {
throw new $TypeError();
@@ -49,13 +53,13 @@ var ThrowTypeError = $gOPD
: throwTypeError;
var hasSymbols = require('has-symbols')();
var hasProto = require('has-proto')();
var getProto = Object.getPrototypeOf || (
hasProto
? function (x) { return x.__proto__; } // eslint-disable-line no-proto
: null
);
var getProto = require('get-proto');
var $ObjectGPO = require('get-proto/Object.getPrototypeOf');
var $ReflectGPO = require('get-proto/Reflect.getPrototypeOf');
var $apply = require('call-bind-apply-helpers/functionApply');
var $call = require('call-bind-apply-helpers/functionCall');
var needsEval = {};
@@ -86,6 +90,7 @@ var INTRINSICS = {
'%Error%': $Error,
'%eval%': eval, // eslint-disable-line no-eval
'%EvalError%': $EvalError,
'%Float16Array%': typeof Float16Array === 'undefined' ? undefined : Float16Array,
'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,
'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,
'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,
@@ -102,7 +107,8 @@ var INTRINSICS = {
'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols || !getProto ? undefined : getProto(new Map()[Symbol.iterator]()),
'%Math%': Math,
'%Number%': Number,
'%Object%': Object,
'%Object%': $Object,
'%Object.getOwnPropertyDescriptor%': $gOPD,
'%parseFloat%': parseFloat,
'%parseInt%': parseInt,
'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,
@@ -128,7 +134,20 @@ var INTRINSICS = {
'%URIError%': $URIError,
'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,
'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet,
'%Function.prototype.call%': $call,
'%Function.prototype.apply%': $apply,
'%Object.defineProperty%': $defineProperty,
'%Object.getPrototypeOf%': $ObjectGPO,
'%Math.abs%': abs,
'%Math.floor%': floor,
'%Math.max%': max,
'%Math.min%': min,
'%Math.pow%': pow,
'%Math.round%': round,
'%Math.sign%': sign,
'%Reflect.getPrototypeOf%': $ReflectGPO
};
if (getProto) {
@@ -223,11 +242,11 @@ var LEGACY_ALIASES = {
var bind = require('function-bind');
var hasOwn = require('hasown');
var $concat = bind.call(Function.call, Array.prototype.concat);
var $spliceApply = bind.call(Function.apply, Array.prototype.splice);
var $replace = bind.call(Function.call, String.prototype.replace);
var $strSlice = bind.call(Function.call, String.prototype.slice);
var $exec = bind.call(Function.call, RegExp.prototype.exec);
var $concat = bind.call($call, Array.prototype.concat);
var $spliceApply = bind.call($apply, Array.prototype.splice);
var $replace = bind.call($call, String.prototype.replace);
var $strSlice = bind.call($call, String.prototype.slice);
var $exec = bind.call($call, RegExp.prototype.exec);
/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */
var rePropName = /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g;

View File

@@ -1,6 +1,6 @@
{
"name": "get-intrinsic",
"version": "1.2.4",
"version": "1.3.0",
"description": "Get and robustly cache all JS language-level intrinsics at first require time",
"main": "index.js",
"exports": {
@@ -17,7 +17,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -43,26 +43,37 @@
"url": "https://github.com/ljharb/get-intrinsic/issues"
},
"homepage": "https://github.com/ljharb/get-intrinsic#readme",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"call-bind": "^1.0.5",
"es-abstract": "^1.22.3",
"es-value-fixtures": "^1.4.2",
"@ljharb/eslint-config": "^21.1.1",
"auto-changelog": "^2.5.0",
"call-bound": "^1.0.3",
"encoding": "^0.1.13",
"es-abstract": "^1.23.9",
"es-value-fixtures": "^1.7.1",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"for-each": "^0.3.5",
"make-async-function": "^1.0.0",
"make-async-generator-function": "^1.0.0",
"make-generator-function": "^2.0.0",
"mock-property": "^1.0.3",
"mock-property": "^1.1.0",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"object-inspect": "^1.13.1",
"object-inspect": "^1.13.4",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4"
"tape": "^5.9.0"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -72,13 +83,6 @@
"backfillLimit": false,
"hideCredit": true
},
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"testling": {
"files": "test/GetIntrinsic.js"
},

View File

@@ -10,10 +10,10 @@ var asyncFns = require('make-async-function').list();
var asyncGenFns = require('make-async-generator-function')();
var mockProperty = require('mock-property');
var callBound = require('call-bind/callBound');
var callBound = require('call-bound');
var v = require('es-value-fixtures');
var $gOPD = require('gopd');
var DefinePropertyOrThrow = require('es-abstract/2021/DefinePropertyOrThrow');
var DefinePropertyOrThrow = require('es-abstract/2023/DefinePropertyOrThrow');
var $isProto = callBound('%Object.prototype.isPrototypeOf%');

View File

@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.2.0](https://github.com/ljharb/gopd/compare/v1.1.0...v1.2.0) - 2024-12-03
### Commits
- [New] add `gOPD` entry point; remove `get-intrinsic` [`5b61232`](https://github.com/ljharb/gopd/commit/5b61232dedea4591a314bcf16101b1961cee024e)
## [v1.1.0](https://github.com/ljharb/gopd/compare/v1.0.1...v1.1.0) - 2024-11-29
### Commits
- [New] add types [`f585e39`](https://github.com/ljharb/gopd/commit/f585e397886d270e4ba84e53d226e4f9ca2eb0e6)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `tape` [`0b8e4fd`](https://github.com/ljharb/gopd/commit/0b8e4fded64397a7726a9daa144a6cc9a5e2edfa)
- [Dev Deps] update `aud`, `npmignore`, `tape` [`48378b2`](https://github.com/ljharb/gopd/commit/48378b2443f09a4f7efbd0fb6c3ee845a6cabcf3)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`78099ee`](https://github.com/ljharb/gopd/commit/78099eeed41bfdc134c912280483689cc8861c31)
- [Tests] replace `aud` with `npm audit` [`4e0d0ac`](https://github.com/ljharb/gopd/commit/4e0d0ac47619d24a75318a8e1f543ee04b2a2632)
- [meta] add missing `engines.node` [`1443316`](https://github.com/ljharb/gopd/commit/14433165d07835c680155b3dfd62d9217d735eca)
- [Deps] update `get-intrinsic` [`eee5f51`](https://github.com/ljharb/gopd/commit/eee5f51769f3dbaf578b70e2a3199116b01aa670)
- [Deps] update `get-intrinsic` [`550c378`](https://github.com/ljharb/gopd/commit/550c3780e3a9c77b62565712a001b4ed64ea61f5)
- [Dev Deps] add missing peer dep [`8c2ecf8`](https://github.com/ljharb/gopd/commit/8c2ecf848122e4e30abfc5b5086fb48b390dce75)
## [v1.0.1](https://github.com/ljharb/gopd/compare/v1.0.0...v1.0.1) - 2022-11-01
### Commits

5
backend/node_modules/gopd/index.js generated vendored
View File

@@ -1,8 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
var $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);
/** @type {import('.')} */
var $gOPD = require('./gOPD');
if ($gOPD) {
try {

View File

@@ -1,10 +1,11 @@
{
"name": "gopd",
"version": "1.0.1",
"version": "1.2.0",
"description": "`Object.getOwnPropertyDescriptor`, but accounts for IE's broken implementation.",
"main": "index.js",
"exports": {
".": "./index.js",
"./gOPD": "./gOPD.js",
"./package.json": "./package.json"
},
"sideEffects": false,
@@ -12,12 +13,13 @@
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prelint": "tsc -p . && attw -P",
"lint": "eslint --ext=js,mjs .",
"postlint": "evalmd README.md",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>=10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -41,19 +43,20 @@
"url": "https://github.com/ljharb/gopd/issues"
},
"homepage": "https://github.com/ljharb/gopd#readme",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.0.0",
"aud": "^2.0.1",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"in-publish": "^2.0.1",
"npmignore": "^0.3.0",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.6.1"
"tape": "^5.9.0",
"typescript": "next"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -67,5 +70,8 @@
"ignore": [
".github/workflows"
]
},
"engines": {
"node": ">= 0.4"
}
}

View File

@@ -10,6 +10,7 @@ test('gOPD', function (t) {
var obj = { x: 1 };
st.ok('x' in obj, 'property exists');
// @ts-expect-error TS can't figure out narrowing from `skip`
var desc = gOPD(obj, 'x');
st.deepEqual(
desc,
@@ -25,7 +26,7 @@ test('gOPD', function (t) {
st.end();
});
t.test('not supported', { skip: gOPD }, function (st) {
t.test('not supported', { skip: !!gOPD }, function (st) {
st.notOk(gOPD, 'is falsy');
st.end();

View File

@@ -1,5 +0,0 @@
{
"root": true,
"extends": "@ljharb",
}

View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/has-proto
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,38 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.3](https://github.com/inspect-js/has-proto/compare/v1.0.2...v1.0.3) - 2024-02-19
### Commits
- [types] add missing declaration file [`26ecade`](https://github.com/inspect-js/has-proto/commit/26ecade05d253bb5dc376945ee3186d1fbe334f8)
## [v1.0.2](https://github.com/inspect-js/has-proto/compare/v1.0.1...v1.0.2) - 2024-02-19
### Commits
- add types [`6435262`](https://github.com/inspect-js/has-proto/commit/64352626cf511c0276d5f4bb6be770a0bf0f8524)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `npmignore`, `tape` [`f16a5e4`](https://github.com/inspect-js/has-proto/commit/f16a5e4121651e551271419f9d60fdd3561fd82c)
- [Refactor] tiny cleanup [`d1f1a4b`](https://github.com/inspect-js/has-proto/commit/d1f1a4bdc135f115a10f148ce302676224534702)
- [meta] add `sideEffects` flag [`e7ab1a6`](https://github.com/inspect-js/has-proto/commit/e7ab1a6f153b3e80dee68d1748b71e46767a0531)
## [v1.0.1](https://github.com/inspect-js/has-proto/compare/v1.0.0...v1.0.1) - 2022-12-21
### Commits
- [meta] correct URLs and description [`ef34483`](https://github.com/inspect-js/has-proto/commit/ef34483ca0d35680f271b6b96e35526151b25dfc)
- [patch] add an additional criteria [`e81959e`](https://github.com/inspect-js/has-proto/commit/e81959ed7c7a77fbf459f00cb4ef824f1099497f)
- [Dev Deps] update `aud` [`2bec2c4`](https://github.com/inspect-js/has-proto/commit/2bec2c47b072b122ff5443fba0263f6dc649531f)
## v1.0.0 - 2022-12-12
### Commits
- Initial implementation, tests, readme [`6886fea`](https://github.com/inspect-js/has-proto/commit/6886fea578f67daf69a7920b2eb7637ea6ebb0bc)
- Initial commit [`99129c8`](https://github.com/inspect-js/has-proto/commit/99129c8f42471ac89cb681ba9cb9d52a583eb94f)
- npm init [`2844ad8`](https://github.com/inspect-js/has-proto/commit/2844ad8e75b84d66a46765b3bab9d2e8ea692e10)
- Only apps should have lockfiles [`c65bc5e`](https://github.com/inspect-js/has-proto/commit/c65bc5e40b9004463f7336d47c67245fb139a36a)

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Inspect JS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,38 +0,0 @@
# has-proto <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]
[![npm badge][npm-badge-png]][package-url]
Does this environment have the ability to set the [[Prototype]] of an object on creation with `__proto__`?
## Example
```js
var hasProto = require('has-proto');
var assert = require('assert');
assert.equal(typeof hasProto(), 'boolean');
```
## Tests
Simply clone the repo, `npm install`, and run `npm test`
[package-url]: https://npmjs.org/package/has-proto
[npm-version-svg]: https://versionbadg.es/inspect-js/has-proto.svg
[deps-svg]: https://david-dm.org/inspect-js/has-proto.svg
[deps-url]: https://david-dm.org/inspect-js/has-proto
[dev-deps-svg]: https://david-dm.org/inspect-js/has-proto/dev-status.svg
[dev-deps-url]: https://david-dm.org/inspect-js/has-proto#info=devDependencies
[npm-badge-png]: https://nodei.co/npm/has-proto.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/has-proto.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/has-proto.svg
[downloads-url]: https://npm-stat.com/charts.html?package=has-proto
[codecov-image]: https://codecov.io/gh/inspect-js/has-proto/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/inspect-js/has-proto/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/inspect-js/has-proto
[actions-url]: https://github.com/inspect-js/has-proto/actions

View File

@@ -1,3 +0,0 @@
declare function hasProto(): boolean;
export = hasProto;

View File

@@ -1,15 +0,0 @@
'use strict';
var test = {
__proto__: null,
foo: {}
};
var $Object = Object;
/** @type {import('.')} */
module.exports = function hasProto() {
// @ts-expect-error: TS errors on an inherited property for some reason
return { __proto__: test }.foo === test.foo
&& !(test instanceof $Object);
};

View File

@@ -1,78 +0,0 @@
{
"name": "has-proto",
"version": "1.0.3",
"description": "Does this environment have the ability to get the [[Prototype]] of an object on creation with `__proto__`?",
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p .",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/inspect-js/has-proto.git"
},
"keywords": [
"prototype",
"proto",
"set",
"get",
"__proto__",
"getPrototypeOf",
"setPrototypeOf",
"has"
],
"author": "Jordan Harband <ljharb@gmail.com>",
"funding": {
"url": "https://github.com/sponsors/ljharb"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/inspect-js/has-proto/issues"
},
"homepage": "https://github.com/inspect-js/has-proto#readme",
"testling": {
"files": "test/index.js"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"eslint": "=8.8.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.5",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",
"unreleased": false,
"commitLimit": false,
"backfillLimit": false,
"hideCredit": true
},
"publishConfig": {
"ignore": [
".github/workflows"
]
}
}

View File

@@ -1,19 +0,0 @@
'use strict';
var test = require('tape');
var hasProto = require('../');
test('hasProto', function (t) {
var result = hasProto();
t.equal(typeof result, 'boolean', 'returns a boolean (' + result + ')');
var obj = { __proto__: null };
if (result) {
t.notOk('toString' in obj, 'null object lacks toString');
} else {
t.ok('toString' in obj, 'without proto, null object has toString');
t.equal(obj.__proto__, null); // eslint-disable-line no-proto
}
t.end();
});

View File

@@ -1,49 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": ["types"], /* Specify multiple folders that act like './node_modules/@types'. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
//"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"coverage"
]
}

View File

@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.1.0](https://github.com/inspect-js/has-symbols/compare/v1.0.3...v1.1.0) - 2024-12-02
### Commits
- [actions] update workflows [`548c0bf`](https://github.com/inspect-js/has-symbols/commit/548c0bf8c9b1235458df7a1c0490b0064647a282)
- [actions] further shard; update action deps [`bec56bb`](https://github.com/inspect-js/has-symbols/commit/bec56bb0fb44b43a786686b944875a3175cf3ff3)
- [meta] use `npmignore` to autogenerate an npmignore file [`ac81032`](https://github.com/inspect-js/has-symbols/commit/ac81032809157e0a079e5264e9ce9b6f1275777e)
- [New] add types [`6469cbf`](https://github.com/inspect-js/has-symbols/commit/6469cbff1866cfe367b2b3d181d9296ec14b2a3d)
- [actions] update rebase action to use reusable workflow [`9c9d4d0`](https://github.com/inspect-js/has-symbols/commit/9c9d4d0d8938e4b267acdf8e421f4e92d1716d72)
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `tape` [`adb5887`](https://github.com/inspect-js/has-symbols/commit/adb5887ca9444849b08beb5caaa9e1d42320cdfb)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`13ec198`](https://github.com/inspect-js/has-symbols/commit/13ec198ec80f1993a87710af1606a1970b22c7cb)
- [Dev Deps] update `auto-changelog`, `core-js`, `tape` [`941be52`](https://github.com/inspect-js/has-symbols/commit/941be5248387cab1da72509b22acf3fdb223f057)
- [Tests] replace `aud` with `npm audit` [`74f49e9`](https://github.com/inspect-js/has-symbols/commit/74f49e9a9d17a443020784234a1c53ce765b3559)
- [Dev Deps] update `npmignore` [`9c0ac04`](https://github.com/inspect-js/has-symbols/commit/9c0ac0452a834f4c2a4b54044f2d6a89f17e9a70)
- [Dev Deps] add missing peer dep [`52337a5`](https://github.com/inspect-js/has-symbols/commit/52337a5621cced61f846f2afdab7707a8132cc12)
## [v1.0.3](https://github.com/inspect-js/has-symbols/compare/v1.0.2...v1.0.3) - 2022-03-01
### Commits

View File

@@ -3,6 +3,7 @@
var origSymbol = typeof Symbol !== 'undefined' && Symbol;
var hasSymbolSham = require('./shams');
/** @type {import('.')} */
module.exports = function hasNativeSymbols() {
if (typeof origSymbol !== 'function') { return false; }
if (typeof Symbol !== 'function') { return false; }

View File

@@ -1,21 +1,23 @@
{
"name": "has-symbols",
"version": "1.0.3",
"version": "1.1.0",
"description": "Determine if the JS environment has Symbol support. Supports spec, or shams.",
"main": "index.js",
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"pretest": "npm run --silent lint",
"test": "npm run tests-only",
"posttest": "aud --production",
"tests-only": "npm run test:stock && npm run test:staging && npm run test:shams",
"posttest": "npx npm@'>=10.2' audit --production",
"tests-only": "npm run test:stock && npm run test:shams",
"test:stock": "nyc node test",
"test:staging": "nyc node --harmony --es-staging test",
"test:shams": "npm run --silent test:shams:getownpropertysymbols && npm run --silent test:shams:corejs",
"test:shams:corejs": "nyc node test/shams/core-js.js",
"test:shams:getownpropertysymbols": "nyc node test/shams/get-own-property-symbols.js",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p . && attw -P",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -54,15 +56,22 @@
},
"homepage": "https://github.com/ljharb/has-symbols#readme",
"devDependencies": {
"@ljharb/eslint-config": "^20.2.3",
"aud": "^2.0.0",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/core-js": "^2.5.8",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"core-js": "^2.6.12",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"get-own-property-symbols": "^0.9.5",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.5.2"
"tape": "^5.9.0",
"typescript": "next"
},
"testling": {
"files": "test/index.js",
@@ -93,9 +102,10 @@
"backfillLimit": false,
"hideCredit": true
},
"greenkeeper": {
"publishConfig": {
"ignore": [
"core-js"
".github/workflows",
"types"
]
}
}

View File

@@ -1,10 +1,12 @@
'use strict';
/** @type {import('./shams')} */
/* eslint complexity: [2, 18], max-statements: [2, 33] */
module.exports = function hasSymbols() {
if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; }
if (typeof Symbol.iterator === 'symbol') { return true; }
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -23,7 +25,7 @@ module.exports = function hasSymbols() {
var symVal = 42;
obj[sym] = symVal;
for (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
for (var _ in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; }
if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; }
@@ -34,7 +36,8 @@ module.exports = function hasSymbols() {
if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; }
if (typeof Object.getOwnPropertyDescriptor === 'function') {
var descriptor = Object.getOwnPropertyDescriptor(obj, sym);
// eslint-disable-next-line no-extra-parens
var descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(obj, sym));
if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; }
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -1,5 +1,6 @@
'use strict';
/** @type {(t: import('tape').Test) => false | void} */
// eslint-disable-next-line consistent-return
module.exports = function runSymbolTests(t) {
t.equal(typeof Symbol, 'function', 'global Symbol is a function');
@@ -31,6 +32,7 @@ module.exports = function runSymbolTests(t) {
t.equal(typeof Object.getOwnPropertySymbols, 'function', 'Object.getOwnPropertySymbols is a function');
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -40,8 +42,8 @@ module.exports = function runSymbolTests(t) {
var symVal = 42;
obj[sym] = symVal;
// eslint-disable-next-line no-restricted-syntax
for (sym in obj) { t.fail('symbol property key was found in for..in of object'); }
// eslint-disable-next-line no-restricted-syntax, no-unused-vars
for (var _ in obj) { t.fail('symbol property key was found in for..in of object'); }
t.deepEqual(Object.keys(obj), [], 'no enumerable own keys on symbol-valued object');
t.deepEqual(Object.getOwnPropertyNames(obj), [], 'no own names on symbol-valued object');

View File

@@ -10,10 +10,12 @@
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csv-parser": "^3.0.0",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"iconv-lite": "^0.6.3",
@@ -30,6 +32,15 @@
"vue-eslint-parser": "9.4.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -820,6 +831,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -828,6 +845,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -965,6 +993,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1094,6 +1135,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1237,6 +1290,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1269,6 +1338,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1325,6 +1403,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1352,13 +1444,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1372,6 +1461,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1762,6 +1878,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1858,16 +2010,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1876,6 +2033,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1922,12 +2092,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1954,10 +2124,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1966,11 +2136,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2414,6 +2587,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2981,6 +3163,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -14,6 +14,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",

View File

@@ -1,4 +1,4 @@
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage } from '../controllers/memberController.js';
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import multer from 'multer';
@@ -13,5 +13,6 @@ router.get('/image/:clubId/:memberId', authenticate, getMemberImage);
router.get('/get/:id/:showAll', authenticate, getClubMembers);
router.post('/set/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, updateRatingsFromMyTischtennis);
export default router;

View File

@@ -0,0 +1,29 @@
import express from 'express';
import myTischtennisController from '../controllers/myTischtennisController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/mytischtennis/account - Get account
router.get('/account', myTischtennisController.getAccount);
// GET /api/mytischtennis/status - Check status
router.get('/status', myTischtennisController.getStatus);
// POST /api/mytischtennis/account - Create or update account
router.post('/account', myTischtennisController.upsertAccount);
// DELETE /api/mytischtennis/account - Delete account
router.delete('/account', myTischtennisController.deleteAccount);
// POST /api/mytischtennis/verify - Verify login
router.post('/verify', myTischtennisController.verifyLogin);
// GET /api/mytischtennis/session - Get stored session
router.get('/session', myTischtennisController.getSession);
export default router;

View File

@@ -23,6 +23,7 @@ router.get('/', authenticate, getAllPredefinedActivities);
router.get('/:id', authenticate, getPredefinedActivityById);
router.put('/:id', authenticate, updatePredefinedActivity);
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.put('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
router.get('/search/query', authenticate, searchPredefinedActivities);
router.post('/merge', authenticate, mergePredefinedActivities);

View File

@@ -8,7 +8,7 @@ import {
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
@@ -33,6 +33,7 @@ import tournamentRoutes from './routes/tournamentRoutes.js';
import accidentRoutes from './routes/accidentRoutes.js';
import trainingStatsRoutes from './routes/trainingStatsRoutes.js';
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -40,7 +41,12 @@ const port = process.env.PORT || 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(cors());
app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
}));
app.use(express.json());
// Globale Fehlerbehandlung, damit der Server bei unerwarteten Fehlern nicht hart abstürzt
@@ -72,6 +78,7 @@ app.use('/api/tournament', tournamentRoutes);
app.use('/api/accident', accidentRoutes);
app.use('/api/training-stats', trainingStatsRoutes);
app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
@@ -171,6 +178,7 @@ app.get('*', (req, res) => {
await safeSync(TournamentResult);
await safeSync(Accident);
await safeSync(UserToken);
await safeSync(MyTischtennis);
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -33,11 +33,11 @@ const login = async (email, password) => {
if (!user || !(await bcrypt.compare(password, user.password))) {
throw { status: 401, message: 'Ungültige Anmeldedaten' };
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '3h' });
await UserToken.create({
userId: user.id,
token,
expiresAt: new Date(Date.now() + 3600 * 1000),
expiresAt: new Date(Date.now() + 3 * 3600 * 1000),
});
return { token };
};

View File

@@ -2,6 +2,7 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import Group from '../models/Group.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
@@ -12,15 +13,47 @@ class DiaryDateActivityService {
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::createActivity] - add: ', data);
const { activity, ...restData } = data;
let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } });
// Versuche, die PredefinedActivity robust zu finden:
// 1) per übergebener ID
// 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein)
// 3) erst dann neu anlegen
let predefinedActivity = null;
if (data.predefinedActivityId) {
predefinedActivity = await PredefinedActivity.findByPk(data.predefinedActivityId);
}
if (!predefinedActivity) {
const normalized = (data.activity || '').trim();
if (normalized) {
predefinedActivity = await PredefinedActivity.findOne({
where: {
[Op.or]: [
{ name: normalized },
{ code: normalized }
]
}
});
}
}
if (!predefinedActivity) {
predefinedActivity = await PredefinedActivity.create({
name: data.activity,
description: '',
duration: data.duration
name: data.name || data.activity || '',
code: data.code || (data.activity || ''),
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null
});
}
restData.predefinedActivityId = predefinedActivity.id;
// Bereinige duration-Feld für DiaryDateActivity
if (restData.duration === '' || restData.duration === undefined) {
restData.duration = null;
} else if (typeof restData.duration === 'string') {
restData.duration = parseInt(restData.duration);
}
const maxOrderId = await DiaryDateActivity.max('orderId', {
where: { diaryDateId: data.diaryDateId }
});
@@ -54,8 +87,8 @@ class DiaryDateActivityService {
console.log('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
predefinedActivity = await PredefinedActivity.create({
name: data.customActivityName,
description: '',
duration: data.duration || activity.duration
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : (activity.duration || null)
});
}
@@ -131,9 +164,7 @@ class DiaryDateActivityService {
}
async getActivities(userToken, clubId, diaryDateId) {
console.log('[DiaryDateActivityService::getActivities] - check user access');
await checkAccess(userToken, clubId);
console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`);
const activities = await DiaryDateActivity.findAll({
where: { diaryDateId },
order: [['orderId', 'ASC']],
@@ -141,6 +172,12 @@ class DiaryDateActivityService {
{
model: PredefinedActivity,
as: 'predefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
{
model: GroupActivity,
@@ -158,8 +195,64 @@ class DiaryDateActivityService {
}
]
});
console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`);
return activities;
// Füge imageUrl zu jeder PredefinedActivity hinzu
const activitiesWithImages = await Promise.all(activities.map(async activity => {
// Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren
const activityData = activity.toJSON();
if (activityData.predefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (firstImage && firstImage.drawingData) {
try {
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
}
}
if (firstImage) {
// Füge sowohl imageUrl als auch imageLink mit Image-ID hinzu
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
} else {
// Fallback: Verwende den Basis-Pfad ohne Image-ID
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
}
}
// Auch für GroupActivities
if (activityData.groupActivities && activityData.groupActivities.length > 0) {
for (const groupActivity of activityData.groupActivities) {
if (groupActivity.groupPredefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const firstImage = await PredefinedActivityImage.findOne({
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
order: [['createdAt', 'ASC']]
});
if (firstImage) {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
} else {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
}
}
}
}
return activityData;
}));
return activitiesWithImages;
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {

View File

@@ -54,7 +54,7 @@ class MemberService {
}
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
picsInInternetAllowed = false, gender = 'unknown') {
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
try {
console.log('[setClubMembers] - Check access');
await checkAccess(userToken, clubId);
@@ -77,6 +77,8 @@ class MemberService {
member.testMembership = testMembership;
member.picsInInternetAllowed = picsInInternetAllowed;
if (gender) member.gender = gender;
if (ttr !== undefined) member.ttr = ttr;
if (qttr !== undefined) member.qttr = qttr;
await member.save();
} else {
await Member.create({
@@ -92,6 +94,8 @@ class MemberService {
testMembership: testMembership,
picsInInternetAllowed: picsInInternetAllowed,
gender: gender || 'unknown',
ttr: ttr,
qttr: qttr,
});
}
console.log('[setClubMembers] - return response');
@@ -146,6 +150,189 @@ class MemberService {
return { status: 500, error: 'Failed to retrieve image' };
}
}
async updateRatingsFromMyTischtennis(userToken, clubId) {
console.log('[updateRatingsFromMyTischtennis] - Check access');
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
console.log('[updateRatingsFromMyTischtennis] - User:', user.id);
const myTischtennisService = (await import('./myTischtennisService.js')).default;
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
try {
// 1. myTischtennis-Session abrufen
console.log('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
const session = await myTischtennisService.getSession(user.id);
console.log('[updateRatingsFromMyTischtennis] - Session retrieved:', {
hasAccessToken: !!session.accessToken,
hasCookie: !!session.cookie,
expiresAt: session.expiresAt
});
const account = await myTischtennisService.getAccount(user.id);
console.log('[updateRatingsFromMyTischtennis] - Account data:', {
id: account?.id,
email: account?.email,
clubId: account?.clubId,
clubName: account?.clubName,
fedNickname: account?.fedNickname,
hasSession: !!(account?.accessToken)
});
if (!account) {
console.error('[updateRatingsFromMyTischtennis] - No account found!');
return {
status: 400,
response: {
message: 'Kein myTischtennis-Account gefunden.',
updated: 0,
errors: [],
debug: { userId: user.id }
}
};
}
if (!account.clubId || !account.fedNickname) {
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
clubId: account.clubId,
fedNickname: account.fedNickname
});
return {
status: 400,
response: {
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
updated: 0,
errors: [],
debug: {
hasClubId: !!account.clubId,
hasFedNickname: !!account.fedNickname,
clubId: account.clubId,
fedNickname: account.fedNickname
}
}
};
}
// 2. Rangliste vom Verein abrufen
console.log('[updateRatingsFromMyTischtennis] - Get club rankings', {
clubId: account.clubId,
fedNickname: account.fedNickname,
hasCookie: !!session.cookie
});
const rankings = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname
);
console.log('[updateRatingsFromMyTischtennis] - Rankings result:', {
success: rankings.success,
entriesCount: rankings.entries?.length || 0,
error: rankings.error
});
if (!rankings.success) {
return {
status: 500,
response: {
message: rankings.error || 'Fehler beim Abrufen der Rangliste',
updated: 0,
errors: [],
debug: {
clubId: account.clubId,
fedNickname: account.fedNickname,
rankingsError: rankings.error
}
}
};
}
// 3. Alle Mitglieder des Clubs laden
console.log('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
const members = await Member.findAll({ where: { clubId } });
console.log('[updateRatingsFromMyTischtennis] - Found members:', members.length);
let updated = 0;
const errors = [];
const notFound = [];
const matched = [];
// 4. Für jedes Mitglied TTR aktualisieren
for (const member of members) {
const firstName = member.firstName;
const lastName = member.lastName;
// Suche nach Match in rankings entries
const rankingEntry = rankings.entries.find(entry =>
entry.firstname.toLowerCase() === firstName.toLowerCase() &&
entry.lastname.toLowerCase() === lastName.toLowerCase()
);
if (rankingEntry) {
try {
// fedRank ist der TTR-Wert
const oldTtr = member.ttr;
member.ttr = rankingEntry.fedRank;
// TODO: QTTR muss von einem anderen Endpoint geholt werden
await member.save();
updated++;
matched.push({
name: `${firstName} ${lastName}`,
oldTtr: oldTtr,
newTtr: rankingEntry.fedRank
});
console.log(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr}${rankingEntry.fedRank}`);
} catch (error) {
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
errors.push({
member: `${firstName} ${lastName}`,
error: error.message
});
}
} else {
notFound.push(`${firstName} ${lastName}`);
console.log(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
}
}
console.log('[updateRatingsFromMyTischtennis] - Update complete');
console.log(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
let message = `${updated} Mitglied(er) aktualisiert.`;
if (notFound.length > 0) {
message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`;
}
if (errors.length > 0) {
message += ` ${errors.length} Fehler beim Speichern.`;
}
return {
status: 200,
response: {
message: message,
updated: updated,
matched: matched,
notFound: notFound,
errors: errors,
totalEntries: rankings.entries.length,
totalMembers: members.length
}
};
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
return {
status: 500,
response: {
message: error.message || 'Fehler beim Aktualisieren',
updated: 0,
errors: [error.message]
}
};
}
}
}
export default new MemberService();

View File

@@ -0,0 +1,256 @@
import MyTischtennis from '../models/MyTischtennis.js';
import User from '../models/User.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisService {
/**
* Get myTischtennis account for user
*/
async getAccount(userId) {
const account = await MyTischtennis.findOne({
where: { userId },
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
});
return account;
}
/**
* Create or update myTischtennis account
*/
async upsertAccount(userId, email, password, savePassword, userPassword) {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError(404, 'Benutzer nicht gefunden');
}
let loginResult = null;
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
if (password) {
const isValidPassword = await user.validatePassword(userPassword);
if (!isValidPassword) {
throw new HttpError(401, 'Ungültiges Passwort');
}
// Login-Versuch bei myTischtennis
loginResult = await myTischtennisClient.login(email, password);
if (!loginResult.success) {
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
}
}
// Find or create account
let account = await MyTischtennis.findOne({ where: { userId } });
const now = new Date();
if (account) {
// Update existing
account.email = email;
account.savePassword = savePassword;
if (password && savePassword) {
account.setPassword(password);
} else if (!savePassword) {
account.encryptedPassword = null;
}
if (loginResult && loginResult.success) {
account.lastLoginAttempt = now;
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
console.log('[myTischtennisService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[myTischtennisService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[myTischtennisService] - Updated account with club data');
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
} else if (password) {
account.lastLoginAttempt = now;
}
await account.save();
} else {
// Create new
const accountData = {
userId,
email,
savePassword,
lastLoginAttempt: password ? now : null,
lastLoginSuccess: loginResult?.success ? now : null
};
if (loginResult && loginResult.success) {
accountData.accessToken = loginResult.accessToken;
accountData.refreshToken = loginResult.refreshToken;
accountData.expiresAt = loginResult.expiresAt;
accountData.cookie = loginResult.cookie;
accountData.userData = loginResult.user;
// Hole Club-ID
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
accountData.clubId = profileResult.clubId;
accountData.clubName = profileResult.clubName;
}
}
account = await MyTischtennis.create(accountData);
if (password && savePassword) {
account.setPassword(password);
await account.save();
}
}
return {
id: account.id,
email: account.email,
savePassword: account.savePassword,
lastLoginAttempt: account.lastLoginAttempt,
lastLoginSuccess: account.lastLoginSuccess,
expiresAt: account.expiresAt
};
}
/**
* Delete myTischtennis account
*/
async deleteAccount(userId) {
const deleted = await MyTischtennis.destroy({
where: { userId }
});
return deleted > 0;
}
/**
* Verify login with stored or provided credentials
*/
async verifyLogin(userId, providedPassword = null) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
let password = providedPassword;
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
if (!password) {
if (!account.savePassword || !account.encryptedPassword) {
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
}
password = account.getPassword();
}
// Login-Versuch
const now = new Date();
account.lastLoginAttempt = now;
const loginResult = await myTischtennisClient.login(account.email, password);
if (loginResult.success) {
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation
console.log('[myTischtennisService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[myTischtennisService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[myTischtennisService] - Updated account with club data');
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
await account.save();
return {
success: true,
accessToken: loginResult.accessToken,
refreshToken: loginResult.refreshToken,
expiresAt: loginResult.expiresAt,
user: loginResult.user,
clubId: account.clubId,
clubName: account.clubName
};
} else {
await account.save(); // Save lastLoginAttempt
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
}
}
/**
* Check if account is configured and ready
*/
async checkAccountStatus(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
return {
exists: !!account,
hasEmail: !!account?.email,
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
needsConfiguration: !account || !account.email,
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
};
}
/**
* Get stored session for user (for authenticated API requests)
*/
async getSession(userId) {
const account = await MyTischtennis.findOne({ where: { userId } });
if (!account) {
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
}
// Check if session is valid
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
return {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
cookie: account.cookie,
expiresAt: account.expiresAt,
userData: account.userData
};
}
}
export default new MyTischtennisService();

View File

@@ -15,6 +15,7 @@ class PredefinedActivityService {
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}
@@ -32,6 +33,7 @@ class PredefinedActivityService {
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}

View File

@@ -88,7 +88,7 @@ class TournamentService {
include: [{
model: Member,
as: 'member',
attributes: ['id', 'firstName', 'lastName'],
attributes: ['id', 'firstName', 'lastName', 'ttr', 'qttr'],
}],
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
});

View File

@@ -67,6 +67,16 @@
</a>
</div>
</nav>
<nav class="nav-menu">
<div class="nav-section">
<h4 class="nav-title">Einstellungen</h4>
<a href="/mytischtennis-account" class="nav-link">
<span class="nav-icon">🔗</span>
myTischtennis-Account
</a>
</div>
</nav>
<div class="sidebar-footer">
<button @click="logout()" class="btn-secondary logout-btn">

View File

@@ -0,0 +1,442 @@
<template>
<div class="render-container">
<canvas
ref="canvas"
:width="config.canvas.width"
:height="config.canvas.height"
style="margin:0 auto;"
></canvas>
</div>
</template>
<script>
export default {
name: 'CourtDrawingRender',
props: {
drawingData: {
type: Object,
required: true
},
width: {
type: Number,
default: 600
},
height: {
type: Number,
default: 400
}
},
data() {
return {
canvas: null,
ctx: null,
config: {
// Mehr Platz links für die drei Startkreise (Rand außerhalb des Tisches)
canvas: { width: 640, height: 400 },
table: {
// Tabelle etwas schmaler und weiter rechts platzieren
width: 500,
height: 300,
color: '#2d5a2d',
borderWidth: 2,
borderColor: '#000000',
outerFrameWidth: 5,
outerFrameColor: '#ffffff',
outerFrameBorderWidth: 0.5,
outerFrameBorderColor: '#000000'
},
horizontalLine: { color: '#cfd8dc', width: 2, gap: 10, edgeMargin: 10 },
net: { color: '#ffffff', width: 4, overhang: 14, borderColor: '#000000', borderWidth: 0.5 },
serviceLine: { color: 'rgba(255,255,255,0.0)', width: 0 },
startCircles: {
radius: 10,
// Abstand vom linken Canvas-Rand (größer = weiter links, da wir von tableX abziehen)
x: 20,
topYOffset: 35,
bottomYOffset: 35,
selectedColor: '#ff4444',
unselectedColor: '#9e9e9e',
selectedBorderColor: '#ffffff',
unselectedBorderColor: '#555555',
selectedBorderWidth: 2,
unselectedBorderWidth: 1
},
arrows: {
primaryColor: '#d32f2f', // rechts -> target (rot)
secondaryColor: '#1565c0', // zurück (blau)
width: 6,
headLength: 24,
vhOffsetX: 5,
vhOffsetY: 8,
rhOffsetX: 10,
rhOffsetY: 0
},
targetCircles: {
radius: 13,
rightXOffset: 20,
middleXOffset: 40,
topYOffset: 45,
bottomYOffset: 45,
transparency: 0.25
},
leftTargetCircles: {
radius: 10,
leftXOffset: 20,
rightXOffset: 40,
topYOffset: 40,
bottomYOffset: 40
},
hitMarker: {
radius: 10.5,
fill: '#ffffff',
stroke: '#000000',
lineWidth: 1
}
}
};
},
watch: {
drawingData: {
handler() {
this.redraw();
},
deep: true,
immediate: true
}
},
mounted() {
// apply custom dimensions if provided
this.config.canvas.width = this.width || 600;
this.config.canvas.height = this.height || 400;
this.$nextTick(() => {
console.log('CourtDrawingRender: mounted with drawingData =', this.drawingData);
this.init();
this.redraw();
});
},
methods: {
init() {
this.canvas = this.$refs.canvas;
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
console.log('CourtDrawingRender: canvas/context initialized');
}
},
redraw() {
console.log('CourtDrawingRender: redraw called with data =', this.drawingData);
if (!this.ctx) return;
const { width, height } = this.config.canvas;
// clear and background
this.ctx.clearRect(0, 0, width, height);
this.ctx.fillStyle = '#f0f0f0';
this.ctx.fillRect(0, 0, width, height);
this.drawTable();
this.drawStartCircles();
this.drawArrowsAndLabels();
},
drawTable() {
const ctx = this.ctx;
const { table, horizontalLine, net, outerFrameWidth } = {
...this.config,
outerFrameWidth: this.config.table.outerFrameWidth
};
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
// table fill
ctx.fillStyle = table.color;
ctx.fillRect(tableX, tableY, tableWidth, tableHeight);
// table stroke
ctx.strokeStyle = table.borderColor;
ctx.lineWidth = table.borderWidth;
ctx.strokeRect(tableX, tableY, tableWidth, tableHeight);
// outer white frame
ctx.strokeStyle = table.outerFrameColor;
ctx.lineWidth = table.outerFrameWidth;
ctx.strokeRect(
tableX - table.outerFrameWidth,
tableY - table.outerFrameWidth,
tableWidth + table.outerFrameWidth * 2,
tableHeight + table.outerFrameWidth * 2
);
// black thin frame around white
ctx.strokeStyle = table.outerFrameBorderColor;
ctx.lineWidth = table.outerFrameBorderWidth;
ctx.strokeRect(
tableX - table.outerFrameWidth - table.outerFrameBorderWidth,
tableY - table.outerFrameWidth - table.outerFrameBorderWidth,
tableWidth + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2,
tableHeight + (table.outerFrameWidth + table.outerFrameBorderWidth) * 2
);
// horizontal split line (two segments)
ctx.strokeStyle = horizontalLine.color;
ctx.lineWidth = horizontalLine.width;
const centerY = tableY + tableHeight / 2;
const centerX = tableX + tableWidth / 2;
const gap = horizontalLine.gap;
const edge = horizontalLine.edgeMargin;
ctx.beginPath();
ctx.moveTo(tableX + edge, centerY);
ctx.lineTo(centerX - gap, centerY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX + gap, centerY);
ctx.lineTo(tableX + tableWidth - edge, centerY);
ctx.stroke();
// net vertical
ctx.strokeStyle = net.color;
ctx.lineWidth = net.width;
ctx.beginPath();
ctx.moveTo(centerX, tableY - net.overhang);
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
ctx.stroke();
ctx.strokeStyle = net.borderColor;
ctx.lineWidth = net.borderWidth;
ctx.beginPath();
ctx.moveTo(centerX, tableY - net.overhang);
ctx.lineTo(centerX, tableY + tableHeight + net.overhang);
ctx.stroke();
},
drawStartCircles() {
const ctx = this.ctx;
const cfg = this.config.startCircles;
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
// Startkreis links VOR dem Tisch positionieren nur den Kreis zeichnen,
// von dem auch der rote Pfeil startet
const circleX = tableX - cfg.x; // Links vom Tisch
const topY = tableY + cfg.topYOffset;
const midY = tableY + tableHeight / 2;
const botY = tableY + tableHeight - cfg.bottomYOffset;
// Mapping und Fallback: bei "AS" auf 'middle'
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const selKey = this.drawingData?.selectedStartPosition || 'AS';
const selectedPos = map[selKey] || 'middle';
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
ctx.fillStyle = cfg.selectedColor;
ctx.beginPath();
ctx.arc(circleX, y, cfg.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = cfg.selectedBorderColor;
ctx.lineWidth = cfg.selectedBorderWidth;
ctx.stroke();
},
computeRightTargetPosition(number) {
// Geometrie wie im Tool: nutzt rightXOffset/middleXOffset
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const cfg = this.config.targetCircles;
const x1 = tableX + tableWidth - cfg.rightXOffset; // (1,2,3)
const x3 = tableX + tableWidth / 2 + cfg.middleXOffset; // (7,8,9)
const xdiff = x3 - x1;
const x2 = x3 - xdiff / 2; // (4,5,6)
const positions = {
1: { x: x1, y: tableY + cfg.topYOffset },
2: { x: x1, y: tableY + tableHeight / 2 },
3: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
4: { x: x2, y: tableY + cfg.topYOffset },
5: { x: x2, y: tableY + tableHeight / 2 },
6: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
7: { x: x3, y: tableY + cfg.topYOffset },
8: { x: x3, y: tableY + tableHeight / 2 },
9: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset }
};
return positions[number] || null;
},
getStartPoint() {
// Startpunkt wie im Tool abhängig von Schlagseite (VH/RH)
const sc = this.config.startCircles;
const ar = this.config.arrows;
const tblW = this.config.table.width;
const tblH = this.config.table.height;
const tableX = (this.config.canvas.width - tblW) / 2;
const tableY = (this.config.canvas.height - tblH) / 2;
const circleX = tableX - sc.x; // Kreis links vor dem Tisch
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const pos = map[this.drawingData?.selectedStartPosition] || 'middle';
const y = pos === 'top' ? tableY + sc.topYOffset : pos === 'bottom' ? tableY + tblH - sc.bottomYOffset : tableY + tblH / 2;
const isVH = (this.drawingData?.strokeType || 'VH') === 'VH';
const startX = isVH
? circleX + sc.radius + ar.vhOffsetX
: circleX + sc.radius + ar.rhOffsetX;
// VH: unterhalb seitlich (circleRadius + vhOffsetY), RH: rechts (rhOffsetY = 0)
const startYOffset = isVH ? (sc.radius + ar.vhOffsetY) : ar.rhOffsetY;
return { x: startX, y: y + startYOffset };
},
drawArrow(ctx, from, to, color, label) {
const { width, headLength } = this.config.arrows;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = width;
// line
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
// arrow head
const angle = Math.atan2(to.y - from.y, to.x - from.x);
ctx.beginPath();
ctx.moveTo(to.x, to.y);
ctx.lineTo(to.x - headLength * Math.cos(angle - Math.PI / 6), to.y - headLength * Math.sin(angle - Math.PI / 6));
ctx.lineTo(to.x - headLength * Math.cos(angle + Math.PI / 6), to.y - headLength * Math.sin(angle + Math.PI / 6));
ctx.closePath();
ctx.fill();
},
drawLabelBelow(ctx, text, anchor) {
if (!text) return;
ctx.font = 'bold 13px Helvetica';
const padding = 6;
const yOffset = 23; // 5px tiefer
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
let x = anchor.x - textWidth / 2; // zentriert unter dem Punkt
let y = anchor.y + yOffset;
// clamp innerhalb Canvas
x = Math.max(4, Math.min(this.config.canvas.width - textWidth - 4, x));
y = Math.max(14, Math.min(this.config.canvas.height - 4, y));
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
},
drawArrowsAndLabels() {
if (!this.drawingData) return;
const ctx = this.ctx;
const from = this.getStartPoint();
// First arrow: to right target
const tp = Number(this.drawingData.targetPosition);
if (tp) {
const to = this.computeRightTargetPosition(tp);
// Zielmarkierung (unter dem Pfeilkopf)
this.drawHitMarker(ctx, to);
const strokeSide = this.drawingData.strokeType || '';
const spinAbbrev = this.abbrevSpin(this.drawingData.spinType);
// Text gehört an die Quelle (ohne "target")
const sourceLabel = `${strokeSide} ${spinAbbrev}`.trim();
const toEnd = { x: to.x - this.config.targetCircles.radius, y: to.y };
this.drawArrow(ctx, from, toEnd, this.config.arrows.primaryColor);
// Unter dem Startkreis beschriften
const startCenter = this.getStartCircleCenter();
this.drawLabelBelow(ctx, sourceLabel, startCenter);
}
// Second arrow (optional): from right source to left target
const leftTarget = this.drawingData.nextStrokeTargetPosition ? Number(this.drawingData.nextStrokeTargetPosition) : null;
if (tp && leftTarget) {
// source near previous right target
const sourceRightCenter = this.computeRightTargetPosition(tp);
// left target mapping: mirror scheme to left half
const toLeftCenter = this.computeLeftTargetPosition(leftTarget);
// Zielmarkierung links
this.drawHitMarker(ctx, toLeftCenter);
const side = this.drawingData.nextStrokeSide || '';
const type = this.drawingData.nextStrokeType || '';
// Text gehört ans Ziel (ohne "extra target")
const targetLabel = `${side} ${type}`.trim();
const sourceRight = { x: sourceRightCenter.x - this.config.targetCircles.radius, y: sourceRightCenter.y };
const toLeft = { x: toLeftCenter.x + this.config.leftTargetCircles.radius, y: toLeftCenter.y };
this.drawArrow(ctx, sourceRight, toLeft, this.config.arrows.secondaryColor);
// Unter dem rechten Ziel (target der ersten Linie) beschriften
this.drawLabelBelow(ctx, targetLabel, sourceRightCenter);
}
},
getStartCircleCenter() {
const cfg = this.config.startCircles;
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const circleX = tableX - cfg.x;
const map = { AS1: 'top', AS2: 'middle', AS3: 'bottom', AS: 'middle' };
const selKey = this.drawingData?.selectedStartPosition || 'AS';
const selectedPos = map[selKey] || 'middle';
const topY = tableY + cfg.topYOffset;
const midY = tableY + tableHeight / 2;
const botY = tableY + tableHeight - cfg.bottomYOffset;
const y = selectedPos === 'top' ? topY : selectedPos === 'bottom' ? botY : midY;
return { x: circleX, y };
},
drawHitMarker(ctx, pos) {
if (!pos) return;
const mk = this.config.hitMarker;
ctx.beginPath();
ctx.arc(pos.x, pos.y, mk.radius, 0, Math.PI * 2);
ctx.fillStyle = mk.fill;
ctx.fill();
ctx.lineWidth = mk.lineWidth;
ctx.strokeStyle = mk.stroke;
ctx.stroke();
},
computeLeftTargetPosition(number) {
// Spiegelung wie im Tool: nutzt leftTargetCircles Offsets
const tableWidth = this.config.table.width;
const tableHeight = this.config.table.height;
const tableX = (this.config.canvas.width - tableWidth) / 2;
const tableY = (this.config.canvas.height - tableHeight) / 2;
const cfg = this.config.leftTargetCircles;
const x1 = tableX + cfg.leftXOffset; // linke Spalte (Lang)
const x3 = tableX + tableWidth / 2 - cfg.rightXOffset; // nah am Netz (Kurz)
const xdiff = x3 - x1;
const x2 = x3 - xdiff / 2;
// Gespiegelte Y-Zuordnung wie im Tool:
// 1,4,7 = unten (VH gespiegelt)
// 2,5,8 = mitte
// 3,6,9 = oben (RH gespiegelt)
const positions = {
1: { x: x1, y: tableY + tableHeight - cfg.bottomYOffset },
2: { x: x1, y: tableY + tableHeight / 2 },
3: { x: x1, y: tableY + cfg.topYOffset },
4: { x: x2, y: tableY + tableHeight - cfg.bottomYOffset },
5: { x: x2, y: tableY + tableHeight / 2 },
6: { x: x2, y: tableY + cfg.topYOffset },
7: { x: x3, y: tableY + tableHeight - cfg.bottomYOffset },
8: { x: x3, y: tableY + tableHeight / 2 },
9: { x: x3, y: tableY + cfg.topYOffset }
};
return positions[number] || null;
},
abbrevSpin(spin) {
if (!spin) return '';
const map = {
Unterschnitt: 'US',
Überschnitt: 'OS',
Seitschnitt: 'SR',
Seitunterschnitt: 'SU',
Gegenläufer: 'GL'
};
return map[spin] || spin;
}
}
};
</script>
<style scoped>
.render-container {
width: 100%;
}
canvas { display: block; max-width: 100%; height: auto; }
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
<template>
<div class="modal-overlay" @click.self="$emit('close')">
<div class="modal">
<div class="modal-header">
<h3>{{ account ? 'myTischtennis-Account bearbeiten' : 'myTischtennis-Account verknüpfen' }}</h3>
</div>
<div class="modal-body">
<div class="form-group">
<label for="mtt-email">myTischtennis-E-Mail:</label>
<input
type="email"
id="mtt-email"
v-model="formData.email"
placeholder="Ihre myTischtennis-E-Mail-Adresse"
required
/>
</div>
<div class="form-group">
<label for="mtt-password">myTischtennis-Passwort:</label>
<input
type="password"
id="mtt-password"
v-model="formData.password"
:placeholder="account && account.savePassword ? 'Leer lassen um beizubehalten' : 'Ihr myTischtennis-Passwort'"
/>
</div>
<div class="form-group checkbox-group">
<label>
<input
type="checkbox"
v-model="formData.savePassword"
/>
<span>myTischtennis-Passwort speichern</span>
</label>
<p class="hint">
Wenn aktiviert, wird Ihr myTischtennis-Passwort verschlüsselt gespeichert,
sodass automatische Synchronisationen möglich sind.
</p>
</div>
<div class="form-group" v-if="formData.password">
<label for="app-password">Ihr App-Passwort zur Bestätigung:</label>
<input
type="password"
id="app-password"
v-model="formData.userPassword"
placeholder="Ihr Passwort für diese App"
required
/>
<p class="hint">
Aus Sicherheitsgründen benötigen wir Ihr App-Passwort,
um das myTischtennis-Passwort zu speichern.
</p>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
Abbrechen
</button>
<button class="btn-primary" @click="saveAccount" :disabled="!canSave || saving">
{{ saving ? 'Speichere...' : 'Speichern' }}
</button>
</div>
</div>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
export default {
name: 'MyTischtennisDialog',
props: {
account: {
type: Object,
default: null
}
},
data() {
return {
formData: {
email: this.account?.email || '',
password: '',
savePassword: this.account?.savePassword || false,
userPassword: ''
},
saving: false,
error: null
};
},
computed: {
canSave() {
// E-Mail ist erforderlich
if (!this.formData.email.trim()) {
return false;
}
// Wenn ein Passwort eingegeben wurde, muss auch das App-Passwort eingegeben werden
if (this.formData.password && !this.formData.userPassword) {
return false;
}
return true;
}
},
methods: {
async saveAccount() {
if (!this.canSave) return;
this.error = null;
this.saving = true;
try {
const payload = {
email: this.formData.email,
savePassword: this.formData.savePassword
};
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
if (this.formData.password) {
payload.password = this.formData.password;
payload.userPassword = this.formData.userPassword;
}
await apiClient.post('/mytischtennis/account', payload);
this.$emit('saved');
} catch (error) {
console.error('Fehler beim Speichern:', error);
this.error = error.response?.data?.message || 'Fehler beim Speichern des Accounts';
} finally {
this.saving = false;
}
}
}
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid #dee2e6;
}
.modal-header h3 {
margin: 0;
color: #495057;
}
.modal-body {
padding: 1.5rem;
flex: 1;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #495057;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.form-group input[type="text"]:focus,
.form-group input[type="email"]:focus,
.form-group input[type="password"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
}
.hint {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #6c757d;
font-style: italic;
}
.error-message {
padding: 0.75rem;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
margin-top: 1rem;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-primary:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: #545b62;
}
.btn-secondary:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>

View File

@@ -13,6 +13,7 @@ import TournamentsView from './views/TournamentsView.vue';
import TrainingStatsView from './views/TrainingStatsView.vue';
import PredefinedActivities from './views/PredefinedActivities.vue';
import OfficialTournaments from './views/OfficialTournaments.vue';
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
import Impressum from './views/Impressum.vue';
import Datenschutz from './views/Datenschutz.vue';
@@ -31,6 +32,7 @@ const routes = [
{ path: '/training-stats', component: TrainingStatsView },
{ path: '/predefined-activities', component: PredefinedActivities },
{ path: '/official-tournaments', component: OfficialTournaments },
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
{ path: '/impressum', component: Impressum },
{ path: '/datenschutz', component: Datenschutz },
];

View File

@@ -126,10 +126,11 @@
</span>
<span v-else @click="startActivityEdit(item)" class="clickable activity-label"
:title="item.predefinedActivity && item.predefinedActivity.name ? item.predefinedActivity.name : ''">
<span v-if="item.predefinedActivity && item.predefinedActivity.imageLink"
@click.stop="showActivityImage(item.predefinedActivity.imageLink)"
class="image-icon"
title="Bild anzeigen">🖼</span>
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
<span v-if="item.predefinedActivity"
@click.stop="openActivityVisual(item.predefinedActivity)"
class="image-icon"
title="Bild/Zeichnung anzeigen">🖼</span>
{{ (item.predefinedActivity && item.predefinedActivity.code && item.predefinedActivity.code.trim() !== '')
? item.predefinedActivity.code
: (item.predefinedActivity ? item.predefinedActivity.name : item.activity) }}
@@ -161,10 +162,12 @@
<td></td>
<td>
<span class="activity-label" :title="(groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.name) ? groupItem.groupPredefinedActivity.name : ''">
<span v-if="groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.imageLink"
@click.stop="showActivityImage(groupItem.groupPredefinedActivity.imageLink)"
class="image-icon"
title="Bild anzeigen">🖼</span>
<!-- Icon öffnet Rendering (falls vorhanden) oder Bild im Modal -->
<span v-if="groupItem.groupPredefinedActivity"
@click.stop="openActivityVisual(groupItem.groupPredefinedActivity)"
class="image-icon"
title="Bild/Zeichnung anzeigen">🖼</span>
{{ (groupItem.groupPredefinedActivity && groupItem.groupPredefinedActivity.code && groupItem.groupPredefinedActivity.code.trim() !== '')
? groupItem.groupPredefinedActivity.code
: groupItem.groupPredefinedActivity.name }}
@@ -319,8 +322,13 @@
</div>
</div>
</div>
<div v-if="showImage" class="memberImage">
<img :src="imageUrl" @click="closeImage" />
<div v-if="showImage || showRenderModal" class="memberImage" @click="closeImage">
<template v-if="showImage">
<img :src="imageUrl" />
</template>
<template v-else>
<CourtDrawingRender :drawing-data="renderModalData" :width="560" :height="360" />
</template>
</div>
<div v-if="showAccidentForm" class="accidentForm">
<form @submit.prevent="submitAccident">
@@ -345,7 +353,7 @@
+ accident.accident}}</li>
</ul>
</form>
</div>
</div>
<!-- Schnell hinzufügen Dialog -->
<div v-if="showQuickAddDialog" class="modal-overlay" @click.self="closeQuickAddDialog">
@@ -397,10 +405,11 @@ import apiClient from '../apiClient.js';
import Multiselect from 'vue-multiselect';
import Sortable from 'sortablejs';
import PDFGenerator from '../components/PDFGenerator.js';
import CourtDrawingRender from '../components/CourtDrawingRender.vue';
export default {
name: 'DiaryView',
components: { Multiselect },
components: { Multiselect, CourtDrawingRender },
data() {
return {
date: null,
@@ -436,6 +445,8 @@ export default {
showDropdown: false,
showImage: false,
imageUrl: '',
showRenderModal: false,
renderModalData: null,
groups: [],
currentTimeBlockId: null,
newGroupName: '',
@@ -526,6 +537,36 @@ export default {
},
},
methods: {
drawingDataFor(pa) {
// Zeichnungsdaten können bereits als Objekt vorliegen oder als JSON-String
try {
if (!pa) return null;
if (pa.drawingData && typeof pa.drawingData === 'object') {
console.debug('DiaryView: drawingData (object) gefunden für', pa.id);
return pa.drawingData;
}
if (pa.drawingData && typeof pa.drawingData === 'string') {
const parsed = JSON.parse(pa.drawingData);
console.debug('DiaryView: drawingData (string→parsed) für', pa.id, parsed);
return parsed;
}
// Fallback: falls über images[0].drawingData geliefert wurde
if (pa.images && pa.images.length) {
// Nimm das erste Bild, das drawingData enthält
const withData = pa.images.find(img => !!img.drawingData);
if (withData) {
const parsedImg = typeof withData.drawingData === 'string'
? JSON.parse(withData.drawingData)
: withData.drawingData;
console.debug('DiaryView: drawingData aus images für', pa.id, 'imageId=', withData.id, parsedImg);
return parsedImg;
}
}
} catch (e) {
console.warn('DiaryView: drawingData parse error:', e);
}
return null;
},
async init() {
if (this.isAuthenticated && this.currentClub) {
const response = await apiClient.get(`/diary/${this.currentClub}`);
@@ -1119,12 +1160,43 @@ export default {
},
closeImage() {
this.showImage = false;
this.showRenderModal = false;
this.renderModalData = null;
this.imageUrl = '';
},
showActivityImage(imageLink) {
this.imageUrl = imageLink;
this.showImage = true;
async showActivityImage(imageLink) {
try {
// Vorherige Object-URL freigeben
if (this.imageUrl && this.imageUrl.startsWith('blob:')) {
URL.revokeObjectURL(this.imageUrl);
}
if (imageLink && imageLink.startsWith('/api/')) {
// Über API mit Auth und als Blob laden → vermeidet ORB
const path = imageLink.replace(/^\/api\//, '');
const resp = await apiClient.get(`/${path}`, { responseType: 'blob' });
this.imageUrl = URL.createObjectURL(resp.data);
} else {
this.imageUrl = imageLink;
}
this.showImage = true;
} catch (e) {
console.error('Bild laden fehlgeschlagen:', e);
alert('Bild konnte nicht geladen werden.');
}
},
async openActivityVisual(pa) {
// Bevorzugt Rendering, ansonsten Bild
const data = this.drawingDataFor(pa);
if (data) {
// Erzeuge temporär ein PNG im Client: Canvas offscreen rendern über Renderer-Komponente ist aufwändig.
// Einfacher: öffne ein kleines Modal mit der Renderer-Komponente statt imageUrl.
this.renderModalData = data;
this.showRenderModal = true;
} else if (pa.imageLink) {
await this.showActivityImage(pa.imageLink);
}
},
async loadMemberImage(member) {
try {
@@ -1910,6 +1982,12 @@ img {
margin: 0 auto;
}
.memberImage > div, .memberImage canvas {
/* falls Komponenten-Inhalt (Renderer) da ist, mittig zeigen */
display: block;
margin: 0 auto;
}
.groups {
display: flex;
flex-direction: row;

View File

@@ -1,8 +1,12 @@
<template>
<div>
<h2>Mitglieder</h2>
<div>
<button @click="createPhoneList">Telefonliste generieren</button>Es werden nur aktive Mitglieder ausgegeben
<div class="action-buttons">
<button @click="createPhoneList">Telefonliste generieren</button>
<span class="info-text">Es werden nur aktive Mitglieder ausgegeben</span>
<button @click="updateRatingsFromMyTischtennis" class="btn-update-ratings" :disabled="isUpdatingRatings">
{{ isUpdatingRatings ? 'Aktualisiere...' : 'TTR/QTTR von myTischtennis aktualisieren' }}
</button>
</div>
<div class="newmember">
<div class="toggle-new-member">
@@ -56,6 +60,7 @@
<th>Bild (Inet?)</th>
<th>Testm.</th>
<th>Name, Vorname</th>
<th>TTR / QTTR</th>
<th>Adresse</th>
<th>Geburtsdatum</th>
<th>Telefon-Nr.</th>
@@ -80,6 +85,14 @@
<span v-if="!member.active && showInactiveMembers" class="inactive-badge">inaktiv</span>
</span>
</td>
<td class="rating-cell">
<span v-if="member.ttr || member.qttr">
<span v-if="member.ttr" class="ttr-value">{{ member.ttr }}</span>
<span v-if="member.ttr && member.qttr" class="rating-separator">/</span>
<span v-if="member.qttr" class="qttr-value">{{ member.qttr }}</span>
</span>
<span v-else class="no-rating">-</span>
</td>
<td>{{ member.street }}, {{ member.city }}</td>
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
<td>{{ member.phone }}</td>
@@ -152,7 +165,8 @@ export default {
selectedImageUrl: null,
testMembership: false,
showInactiveMembers: false,
newPicsInInternetAllowed: false
newPicsInInternetAllowed: false,
isUpdatingRatings: false
}
},
async mounted() {
@@ -371,6 +385,29 @@ export default {
if (v === 'female') return '♀';
if (v === 'diverse') return '⚧';
return '';
},
async updateRatingsFromMyTischtennis() {
if (!confirm('TTR/QTTR-Werte von myTischtennis aktualisieren?')) {
return;
}
this.isUpdatingRatings = true;
try {
const response = await apiClient.post(`/clubmembers/update-ratings/${this.currentClub}`);
if (response.data.message) {
alert(response.data.message);
}
// Mitglieder neu laden um aktualisierte Werte anzuzeigen
await this.loadMembers();
} catch (error) {
console.error('Fehler beim Aktualisieren der Ratings:', error);
const message = error.response?.data?.error || error.response?.data?.message || 'Fehler beim Aktualisieren der TTR/QTTR-Werte';
alert(message);
} finally {
this.isUpdatingRatings = false;
}
}
}
}
@@ -489,4 +526,61 @@ table td {
.row-inactive { opacity: .6; }
.is-inactive { text-decoration: line-through; }
.inactive-badge { margin-left: .5rem; font-size: .85em; color: #666; text-transform: lowercase; }
.rating-cell {
font-family: 'Courier New', monospace;
font-size: 0.95em;
}
.ttr-value {
font-weight: 600;
color: #1a73e8;
}
.qttr-value {
font-weight: 600;
color: #d81b60;
}
.rating-separator {
color: #999;
margin: 0 0.25rem;
}
.no-rating {
color: #999;
}
.action-buttons {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.info-text {
font-size: 0.9em;
color: #666;
}
.btn-update-ratings {
background-color: #28a745;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
}
.btn-update-ratings:hover:not(:disabled) {
background-color: #218838;
}
.btn-update-ratings:disabled {
background-color: #6c757d;
cursor: not-allowed;
opacity: 0.6;
}
</style>

View File

@@ -0,0 +1,423 @@
<template>
<div class="page-container">
<h1>myTischtennis-Account</h1>
<div class="account-container">
<div v-if="loading" class="loading">Lade...</div>
<div v-else-if="account" class="account-info">
<div class="info-section">
<h2>Verknüpfter Account</h2>
<div class="info-row">
<label>E-Mail:</label>
<span>{{ account.email }}</span>
</div>
<div class="info-row">
<label>Passwort gespeichert:</label>
<span>{{ account.savePassword ? 'Ja' : 'Nein' }}</span>
</div>
<div class="info-row" v-if="account.clubId">
<label>Verein (myTischtennis):</label>
<span>{{ account.clubName }} ({{ account.clubId }}{{ account.fedNickname ? ' - ' + account.fedNickname : '' }})</span>
</div>
<div class="info-row" v-if="account.lastLoginSuccess">
<label>Letzter erfolgreicher Login:</label>
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
</div>
<div class="info-row" v-if="account.lastLoginAttempt">
<label>Letzter Login-Versuch:</label>
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
</div>
<div class="button-group">
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
<button class="btn-secondary" @click="testConnection">Verbindung testen</button>
<button class="btn-secondary" @click="testLoginFlow">Test: Login-Flow</button>
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
</div>
</div>
<!-- Test-Ausgabe -->
<div v-if="testResult" class="test-result" :class="testResult.type">
<h3>Test-Ergebnis:</h3>
<pre>{{ testResult.data }}</pre>
</div>
</div>
<div v-else class="no-account">
<p>Kein myTischtennis-Account verknüpft.</p>
<button class="btn-primary" @click="openEditDialog">Account verknüpfen</button>
</div>
<div class="info-box">
<h3>Über myTischtennis</h3>
<p>Durch die Verknüpfung Ihres myTischtennis-Accounts können Sie:</p>
<ul>
<li>Automatisch Turnierdaten importieren</li>
<li>Spielerergebnisse synchronisieren</li>
<li>Wettkampfdaten direkt abrufen</li>
</ul>
<p><strong>Hinweis:</strong> Das Speichern des Passworts ist optional. Wenn Sie es nicht speichern, werden Sie bei jeder Synchronisation nach dem Passwort gefragt.</p>
</div>
</div>
<!-- Edit Dialog -->
<MyTischtennisDialog
v-if="showDialog"
:account="account"
@close="closeDialog"
@saved="onAccountSaved"
/>
</div>
</template>
<script>
import apiClient from '../apiClient.js';
import MyTischtennisDialog from '../components/MyTischtennisDialog.vue';
export default {
name: 'MyTischtennisAccount',
components: {
MyTischtennisDialog
},
data() {
return {
loading: true,
account: null,
showDialog: false,
testResult: null
};
},
mounted() {
this.loadAccount();
},
methods: {
async loadAccount() {
try {
this.loading = true;
const response = await apiClient.get('/mytischtennis/account');
this.account = response.data.account;
} catch (error) {
console.error('Fehler beim Laden des Accounts:', error);
this.$store.dispatch('showMessage', {
text: 'Fehler beim Laden des myTischtennis-Accounts',
type: 'error'
});
} finally {
this.loading = false;
}
},
openEditDialog() {
this.showDialog = true;
},
closeDialog() {
this.showDialog = false;
},
async onAccountSaved() {
this.closeDialog();
await this.loadAccount();
this.$store.dispatch('showMessage', {
text: 'myTischtennis-Account erfolgreich gespeichert',
type: 'success'
});
},
async testConnection() {
this.testResult = null;
try {
await apiClient.post('/mytischtennis/verify');
this.$store.dispatch('showMessage', {
text: 'Verbindung erfolgreich! Login funktioniert.',
type: 'success'
});
await this.loadAccount(); // Aktualisiere lastLoginSuccess
} catch (error) {
const message = error.response?.data?.message || 'Verbindung fehlgeschlagen';
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
// Passwort-Dialog öffnen
this.showDialog = true;
}
this.$store.dispatch('showMessage', {
text: message,
type: 'error'
});
}
},
async testLoginFlow() {
this.testResult = null;
try {
// 1. Verify Login
console.log('Testing login...');
const verifyResponse = await apiClient.post('/mytischtennis/verify');
console.log('Login successful:', verifyResponse.data);
// 2. Get Session
console.log('Fetching session...');
const sessionResponse = await apiClient.get('/mytischtennis/session');
console.log('Session data:', sessionResponse.data);
// 3. Check Status
console.log('Checking status...');
const statusResponse = await apiClient.get('/mytischtennis/status');
console.log('Status:', statusResponse.data);
this.testResult = {
type: 'success',
data: {
message: 'Alle Tests erfolgreich!',
login: {
accessToken: verifyResponse.data.accessToken ? '✓ vorhanden' : '✗ fehlt',
expiresAt: verifyResponse.data.expiresAt,
clubId: verifyResponse.data.clubId || '✗ nicht gefunden',
clubName: verifyResponse.data.clubName || '✗ nicht gefunden'
},
session: {
accessToken: sessionResponse.data.session?.accessToken ? '✓ vorhanden' : '✗ fehlt',
refreshToken: sessionResponse.data.session?.refreshToken ? '✓ vorhanden' : '✗ fehlt',
cookie: sessionResponse.data.session?.cookie ? '✓ vorhanden' : '✗ fehlt',
userData: sessionResponse.data.session?.userData ? '✓ vorhanden' : '✗ fehlt',
expiresAt: sessionResponse.data.session?.expiresAt
},
status: statusResponse.data
}
};
this.$store.dispatch('showMessage', {
text: 'Test erfolgreich! Details siehe unten.',
type: 'success'
});
} catch (error) {
console.error('Test failed:', error);
this.testResult = {
type: 'error',
data: {
message: 'Test fehlgeschlagen',
error: error.response?.data?.message || error.message,
status: error.response?.status,
details: error.response?.data
}
};
this.$store.dispatch('showMessage', {
text: `Test fehlgeschlagen: ${error.response?.data?.message || error.message}`,
type: 'error'
});
}
},
async deleteAccount() {
if (!confirm('Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?')) {
return;
}
try {
await apiClient.delete('/mytischtennis/account');
this.account = null;
this.$store.dispatch('showMessage', {
text: 'myTischtennis-Account erfolgreich getrennt',
type: 'success'
});
} catch (error) {
console.error('Fehler beim Löschen des Accounts:', error);
this.$store.dispatch('showMessage', {
text: 'Fehler beim Trennen des Accounts',
type: 'error'
});
}
},
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleString('de-DE', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
};
</script>
<style scoped>
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: var(--text-color, #333);
margin-bottom: 2rem;
}
.account-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.account-info, .no-account {
background: white;
border-radius: 8px;
padding: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.info-section h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: var(--primary-color, #007bff);
}
.info-row {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
}
.info-row:last-of-type {
border-bottom: none;
}
.info-row label {
font-weight: 600;
color: #555;
}
.info-row span {
color: #333;
}
.button-group {
display: flex;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.no-account {
text-align: center;
}
.no-account p {
margin-bottom: 1.5rem;
color: #666;
}
.info-box {
background: #f8f9fa;
border-left: 4px solid var(--primary-color, #007bff);
padding: 1.5rem;
border-radius: 4px;
}
.info-box h3 {
margin-top: 0;
color: var(--primary-color, #007bff);
}
.info-box ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.info-box li {
margin: 0.5rem 0;
}
.btn-primary, .btn-secondary, .btn-danger {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.test-result {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
}
.test-result.success {
border-left: 4px solid #28a745;
}
.test-result.error {
border-left: 4px solid #dc3545;
}
.test-result h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
}
.test-result pre {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -56,7 +56,7 @@
</label>
<div class="image-section">
<h4>Bild hinzufügen</h4>
<p class="image-help">Du kannst entweder einen Link zu einem Bild eingeben oder ein Bild hochladen:</p>
<p class="image-help">Du kannst entweder einen Link zu einem Bild eingeben, ein Bild hochladen oder eine Übungszeichnung erstellen:</p>
<label>Bild-Link (optional)
<input type="text" v-model="editModel.imageLink" placeholder="z.B. https://example.com/bild.jpg oder /api/predefined-activities/:id/image/:imageId" />
@@ -74,6 +74,18 @@
</p>
</div>
<!-- Zeichen-Tool -->
<div class="drawing-section">
<h5>Übungszeichnung erstellen</h5>
<CourtDrawingTool
:activity-id="editModel.id"
:drawing-data="editModel.drawingData"
:allow-image-upload="false"
@update-fields="onUpdateFields"
@update-drawing-data="onUpdateDrawingData"
/>
</div>
<div class="image-list" v-if="images && images.length">
<h5>Hochgeladene Bilder:</h5>
<div class="image-grid">
@@ -97,9 +109,13 @@
<script>
import apiClient from '../apiClient.js';
import CourtDrawingTool from '../components/CourtDrawingTool.vue';
export default {
name: 'PredefinedActivities',
components: {
CourtDrawingTool
},
data() {
return {
activities: [],
@@ -107,6 +123,7 @@ export default {
editModel: null,
images: [],
selectedFile: null,
selectedDrawingData: null,
mergeSourceId: '',
mergeTargetId: '',
};
@@ -131,6 +148,15 @@ export default {
}
},
methods: {
parseDrawingData(value) {
if (!value) return null;
if (typeof value === 'object') return value;
try { return JSON.parse(value); } catch (e) { return null; }
},
normalizeActivity(activity, images) {
const drawingData = this.parseDrawingData(activity && activity.drawingData);
return { ...(activity || {}), drawingData };
},
async reload() {
const r = await apiClient.get('/predefined-activities');
this.activities = r.data || [];
@@ -140,7 +166,24 @@ export default {
const r = await apiClient.get(`/predefined-activities/${a.id}`);
const { images, ...activity } = r.data;
this.images = images || [];
this.editModel = { ...activity };
// Server-Daten normalisieren und ggf. image-drawingData fallbacken
let model = this.normalizeActivity(activity, images);
if (!model.drawingData && images && images.length > 0 && images[0].drawingData) {
model.drawingData = this.parseDrawingData(images[0].drawingData);
}
this.editModel = model;
},
async reloadImages() {
if (this.editModel && this.editModel.id) {
try {
const r = await apiClient.get(`/predefined-activities/${this.editModel.id}`);
const { images } = r.data;
this.images = images || [];
console.log('Images reloaded:', this.images);
} catch (error) {
console.error('Error reloading images:', error);
}
}
},
formatItem(a) {
return `${a.code ? '[' + a.code + '] ' : ''}${a.name}`;
@@ -164,6 +207,7 @@ export default {
duration: null,
durationText: '',
imageLink: '',
drawingData: '',
};
},
cancel() {
@@ -173,36 +217,81 @@ export default {
},
async save() {
if (!this.editModel) return;
console.log('Save: selectedFile =', this.selectedFile);
if (this.editModel.id) {
const { id, ...payload } = this.editModel;
if (payload.drawingData && typeof payload.drawingData === 'object') {
payload.drawingData = payload.drawingData;
}
const r = await apiClient.put(`/predefined-activities/${id}`, payload);
this.editModel = r.data;
this.editModel = this.normalizeActivity(r.data);
} else {
const r = await apiClient.post('/predefined-activities', this.editModel);
this.editModel = r.data;
// Nach dem Erstellen einer neuen Aktivität, falls ein Bild ausgewählt wurde, hochladen
if (this.selectedFile) {
await this.uploadImage();
}
this.editModel = this.normalizeActivity(r.data);
}
// Nach dem Speichern (sowohl CREATE als auch UPDATE): Bild hochladen falls vorhanden
if (this.selectedFile) {
console.log('Uploading image after save...');
await this.uploadImage();
} else {
console.log('No selectedFile to upload');
}
await this.reload();
},
onFileChange(e) {
this.selectedFile = e.target.files && e.target.files[0] ? e.target.files[0] : null;
},
imageUrl(img) {
return `/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
return `http://localhost:3000/api/predefined-activities/${this.editModel.id}/image/${img.id}`;
},
async uploadImage() {
if (!this.editModel || !this.editModel.id || !this.selectedFile) return;
if (!this.editModel || !this.editModel.id || !this.selectedFile) {
console.log('Upload skipped: editModel=', this.editModel, 'selectedFile=', this.selectedFile);
return;
}
console.log('Starting image upload...');
console.log('editModel:', this.editModel);
console.log('selectedActivity:', this.selectedActivity);
console.log('Activity ID (editModel.id):', this.editModel.id);
console.log('Activity ID (selectedActivity.id):', this.selectedActivity?.id);
console.log('File:', this.selectedFile);
const fd = new FormData();
fd.append('image', this.selectedFile);
await apiClient.post(`/predefined-activities/${this.editModel.id}/image`, fd, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// Nach Upload Details neu laden
await this.select(this.editModel);
this.selectedFile = null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (this.selectedDrawingData) {
fd.append('drawingData', JSON.stringify(this.selectedDrawingData));
console.log('Added drawingData to FormData:', this.selectedDrawingData);
}
// Verwende PUT für Updates, POST für neue Activities
const isUpdate = this.selectedActivity && this.selectedActivity.id === this.editModel.id;
const method = isUpdate ? 'put' : 'post';
console.log('Using method:', method);
try {
const response = await apiClient[method](`/predefined-activities/${this.editModel.id}/image`, fd, {
headers: { 'Content-Type': 'multipart/form-data' }
});
console.log('Upload successful:', response);
// Nach Upload Details neu laden
await this.select(this.editModel);
this.selectedFile = null;
// Bildliste explizit aktualisieren
await this.reloadImages();
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
},
async deleteImage(imageId) {
if (!this.editModel || !this.editModel.id) return;
@@ -220,6 +309,53 @@ export default {
if (!confirm('Alle Aktivitäten mit identischem Namen werden zusammengeführt. Fortfahren?')) return;
await apiClient.post('/predefined-activities/deduplicate', {});
await this.reload();
},
onDrawingSave(drawingData) {
if (this.editModel) {
this.editModel.drawingData = drawingData;
// Automatisch als Bild-Link setzen, wenn es eine Zeichnung gibt
if (drawingData && !this.editModel.imageLink) {
this.editModel.imageLink = drawingData;
}
}
// Nicht automatisch speichern, nur wenn User explizit "Speichern" klickt
},
onUpdateFields(fields) {
if (this.editModel) {
this.editModel.code = fields.code;
this.editModel.name = fields.name;
this.editModel.description = fields.description;
}
},
onUpdateDrawingData(data) {
if (this.editModel) {
this.editModel.drawingData = data;
}
},
async onDrawingImageUpload(file, drawingData) {
console.log('onDrawingImageUpload called with file:', file);
console.log('onDrawingImageUpload called with drawingData:', drawingData);
console.log('File type:', file?.type);
console.log('File size:', file?.size);
console.log('File name:', file?.name);
// Setze das File und die Zeichnungsdaten für den Upload
this.selectedFile = file;
this.selectedDrawingData = drawingData;
console.log('selectedFile set to:', this.selectedFile);
console.log('selectedDrawingData set to:', this.selectedDrawingData);
// Upload wird erst beim Speichern durchgeführt
console.log('File and drawing data ready for upload when saving');
},
async onImageUploaded() {
console.log('Image uploaded successfully, refreshing image list...');
// Bildliste aktualisieren
if (this.editModel && this.editModel.id) {
await this.select(this.editModel);
}
}
},
async mounted() {
@@ -354,5 +490,19 @@ input[type="text"], input[type="number"], textarea { width: 100%; }
.btn-danger:hover {
background: #c82333;
}
.drawing-section {
margin: 1rem 0;
padding: 1rem;
background: white;
border-radius: var(--border-radius);
border: 1px solid #ddd;
}
.drawing-section h5 {
margin: 0 0 1rem 0;
color: #333;
font-size: 1rem;
}
</style>